minecraft-renderer 0.1.72 → 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 (62) hide show
  1. package/README.md +1 -1
  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 +249 -78
  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 +2 -7
  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.ts +14 -6
  17. package/src/mesher-shared/blockEntityMetadata.test.ts +33 -0
  18. package/src/mesher-shared/blockEntityMetadata.ts +19 -3
  19. package/src/mesher-shared/exportedGeometryTypes.ts +11 -0
  20. package/src/mesher-shared/models.ts +161 -92
  21. package/src/mesher-shared/shared.ts +15 -4
  22. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +40 -0
  23. package/src/mesher-shared/world.ts +12 -0
  24. package/src/mesher-shared/worldLighting.test.ts +54 -0
  25. package/src/playground/baseScene.ts +1 -1
  26. package/src/three/bannerRenderer.ts +10 -3
  27. package/src/three/chunkMeshManager.ts +663 -69
  28. package/src/three/cubeDrawSpans.ts +74 -0
  29. package/src/three/cubeMultiDraw.ts +119 -0
  30. package/src/three/documentRenderer.ts +0 -2
  31. package/src/three/entities.ts +5 -6
  32. package/src/three/entity/EntityMesh.ts +7 -5
  33. package/src/three/entity/gltfAnimationUtils.ts +5 -3
  34. package/src/three/globalBlockBuffer.ts +208 -12
  35. package/src/three/globalLegacyBuffer.ts +701 -0
  36. package/src/three/itemMesh.ts +5 -2
  37. package/src/three/legacySectionCull.ts +85 -0
  38. package/src/three/modules/sciFiWorldReveal.ts +347 -703
  39. package/src/three/modules/starfield.ts +3 -2
  40. package/src/three/sectionRaycastAabb.ts +25 -0
  41. package/src/three/shaders/cubeBlockShader.ts +80 -17
  42. package/src/three/shaders/legacyBlockShader.ts +292 -0
  43. package/src/three/skyboxRenderer.ts +1 -1
  44. package/src/three/tests/chunkMeshManagerLegacy.test.ts +286 -0
  45. package/src/three/tests/cubeDrawSpans.test.ts +73 -0
  46. package/src/three/tests/globalLegacyBuffer.test.ts +360 -0
  47. package/src/three/tests/legacySectionCull.test.ts +80 -0
  48. package/src/three/tests/signTextureCache.test.ts +83 -0
  49. package/src/three/threeJsMedia.ts +2 -2
  50. package/src/three/waypointSprite.ts +2 -2
  51. package/src/three/world/cursorBlock.ts +1 -0
  52. package/src/three/world/vr.ts +2 -2
  53. package/src/three/worldGeometryExport.ts +83 -26
  54. package/src/three/worldRendererThree.ts +94 -25
  55. package/src/wasm-mesher/bridge/render-from-wasm.ts +214 -72
  56. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +18 -6
  57. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  58. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +20 -0
  59. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +67 -5
  60. package/src/wasm-mesher/worker/mesherWasm.ts +70 -14
  61. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +11 -0
  62. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +15 -0
@@ -12,9 +12,11 @@ import { Vec3 } from 'vec3'
12
12
  import { elemFaces, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 } from '../../mesher-shared/modelsGeometryCommon'
13
13
  import type { ExportedWorldGeometry, ExportedSection } from '../../mesher-shared/exportedGeometryTypes'
14
14
  import type { MesherGeometryOutput } from '../../mesher-shared/shared'
15
+ import { bakeLegacyVertexColors } from '../../lib/bakeLegacyLight'
15
16
  import { SECTION_HEIGHT } from '../../mesher-shared/shared'
16
17
  import type { World } from '../../mesher-shared/world'
17
18
  import { resolveBlockPropertiesForMeshing } from '../../mesher-shared/blockPropertiesForMeshing'
19
+ import { isSemiTransparentBlockName } from '../../mesher-shared/models'
18
20
  import {
19
21
  buildShaderCubesFromWords,
20
22
  getShaderCubeResources,
@@ -87,8 +89,10 @@ interface WasmBlockFaceData {
87
89
  block_state_id: number
88
90
  visible_faces: number
89
91
  ao_data: number[][]
90
- light_data: number[][]
91
- light_combined?: number[][] // Packed combined light for shader path ([u8; 4] per visible face)
92
+ light_data?: number[][]
93
+ sky_light_data?: number[][]
94
+ block_light_data?: number[][]
95
+ light_combined?: number[][]
92
96
  }
93
97
 
94
98
  export interface WasmGeometryOutput {
@@ -150,6 +154,78 @@ function computeMesherVertexLight(
150
154
  return vertexLightFromAo(ao, cornerLight15, sideShading, shadingTheme)
151
155
  }
152
156
 
157
+ function vertexTintAoColor (
158
+ world: World | undefined,
159
+ tint: [number, number, number],
160
+ ao: number,
161
+ faceDir: [number, number, number],
162
+ ): [number, number, number] {
163
+ const shadingTheme = world?.config.shadingTheme ?? 'high-contrast'
164
+ const cardinalLight = world?.config.cardinalLight ?? 'default'
165
+ const sideShading = getSideShading(faceDir, shadingTheme, cardinalLight)
166
+ if (shadingTheme === 'high-contrast') {
167
+ const f = sideShading * ((ao + 1) / 4)
168
+ return [tint[0] * f, tint[1] * f, tint[2] * f]
169
+ }
170
+ const f = sideShading * (ao * 0.2 + 0.4)
171
+ return [tint[0] * f, tint[1] * f, tint[2] * f]
172
+ }
173
+
174
+ function sampleChannelLightAt (world: World, pos: Vec3): { block: number, sky: number } {
175
+ return world.getChannelLightNorm(pos)
176
+ }
177
+
178
+ function smoothChannelLightAt (
179
+ world: World,
180
+ cursor: Vec3,
181
+ faceDir: [number, number, number],
182
+ cornerOffset: [number, number, number],
183
+ faceIdx: number,
184
+ ): { block: number, sky: number } {
185
+ const neighbor = cursor.offset(faceDir[0], faceDir[1], faceDir[2])
186
+ const base = sampleChannelLightAt(world, neighbor)
187
+
188
+ if (!world.config.smoothLighting) {
189
+ return base
190
+ }
191
+
192
+ const mask1 = [
193
+ [1, 1, 0], [1, 1, 0], [1, 1, 0], [1, 1, 0], [1, 0, 1], [1, 0, 1],
194
+ ][faceIdx]!
195
+ const mask2 = [
196
+ [0, 1, 1], [0, 1, 1], [1, 0, 1], [1, 0, 1], [0, 1, 1], [0, 1, 1],
197
+ ][faceIdx]!
198
+ const [cx, cy, cz] = cornerOffset
199
+ const [fx, fy, fz] = faceDir
200
+
201
+ const shrink = (v: [number, number, number], mask: number[]) => {
202
+ const out: [number, number, number] = [cx * mask[0]!, cy * mask[1]!, cz * mask[2]!]
203
+ if (fx !== 0) out[0] = 0
204
+ if (fy !== 0) out[1] = 0
205
+ if (fz !== 0) out[2] = 0
206
+ return out
207
+ }
208
+
209
+ const s1 = shrink([cx, cy, cz], mask1)
210
+ const s2 = shrink([cx, cy, cz], mask2)
211
+ const c = shrink([cx, cy, cz], [1, 1, 1])
212
+
213
+ const samples = [
214
+ base,
215
+ sampleChannelLightAt(world, neighbor.offset(s1[0], s1[1], s1[2])),
216
+ sampleChannelLightAt(world, neighbor.offset(s2[0], s2[1], s2[2])),
217
+ sampleChannelLightAt(world, neighbor.offset(c[0], c[1], c[2])),
218
+ ]
219
+
220
+ let blockSum = 0
221
+ let skySum = 0
222
+ for (const s of samples) {
223
+ blockSum += s.block
224
+ skySum += s.sky
225
+ }
226
+ return { block: blockSum / 4, sky: skySum / 4 }
227
+ }
228
+
153
229
  /**
154
230
  * Get or create cached block model with precomputed matrices
155
231
  */
@@ -351,6 +427,8 @@ const renderLiquidToGeometry = (
351
427
  positions: number[],
352
428
  normals: number[],
353
429
  colors: number[],
430
+ skyLights: number[],
431
+ blockLights: number[],
354
432
  uvs: number[],
355
433
  indices: number[],
356
434
  ) => {
@@ -394,7 +472,7 @@ const renderLiquidToGeometry = (
394
472
  const su = texture.su || 1
395
473
  const sv = texture.sv || 1
396
474
 
397
- const baseLight = world.getLight(neighborPos, undefined, undefined, water ? 'water' : 'lava') / 15
475
+ const baseChannels = sampleChannelLightAt(world, neighborPos)
398
476
 
399
477
  const baseIndex = positions.length / 3
400
478
 
@@ -411,7 +489,8 @@ const renderLiquidToGeometry = (
411
489
  normals.push(dir[0], dir[1], dir[2])
412
490
  uvs.push(pos[3] * su + u, pos[4] * sv * (pos[1] ? 1 : height) + v)
413
491
 
414
- let cornerLightResult = baseLight
492
+ let skyNorm = baseChannels.sky
493
+ let blockNorm = baseChannels.block
415
494
  if (world.config.smoothLighting) {
416
495
  const dx = pos[0] * 2 - 1
417
496
  const dy = pos[1] * 2 - 1
@@ -423,16 +502,19 @@ const renderLiquidToGeometry = (
423
502
  const dirVec = new Vec3(dir[0], dir[1], dir[2])
424
503
 
425
504
  const side1LightDir = getVec(new Vec3(side1Dir[0], side1Dir[1], side1Dir[2]), dirVec)
426
- const side1Light = world.getLight(cursor.plus(side1LightDir)) / 15
427
- const side2DirLight = getVec(new Vec3(side2Dir[0], side2Dir[1], side2Dir[2]), dirVec)
428
- const side2Light = world.getLight(cursor.plus(side2DirLight)) / 15
505
+ const side2LightDir = getVec(new Vec3(side2Dir[0], side2Dir[1], side2Dir[2]), dirVec)
429
506
  const cornerLightDir = getVec(new Vec3(cornerDir[0], cornerDir[1], cornerDir[2]), dirVec)
430
- const cornerLight = world.getLight(cursor.plus(cornerLightDir)) / 15
431
507
 
432
- cornerLightResult = (side1Light + side2Light + cornerLight + baseLight) / 4
508
+ const s1 = sampleChannelLightAt(world, cursor.plus(side1LightDir))
509
+ const s2 = sampleChannelLightAt(world, cursor.plus(side2LightDir))
510
+ const sc = sampleChannelLightAt(world, cursor.plus(cornerLightDir))
511
+ blockNorm = (s1.block + s2.block + sc.block + baseChannels.block) / 4
512
+ skyNorm = (s1.sky + s2.sky + sc.sky + baseChannels.sky) / 4
433
513
  }
434
514
 
435
- colors.push(tint[0] * cornerLightResult, tint[1] * cornerLightResult, tint[2] * cornerLightResult)
515
+ colors.push(tint[0], tint[1], tint[2])
516
+ skyLights.push(skyNorm)
517
+ blockLights.push(blockNorm)
436
518
  }
437
519
 
438
520
  indices.push(
@@ -442,12 +524,26 @@ const renderLiquidToGeometry = (
442
524
  baseIndex + 2,
443
525
  baseIndex + 1,
444
526
  baseIndex + 3,
445
- baseIndex,
446
- baseIndex + 2,
447
- baseIndex + 1,
448
- baseIndex + 2,
449
- baseIndex + 3,
450
- baseIndex + 1,
527
+ )
528
+
529
+ const dupBase = positions.length / 3
530
+ for (let v = 0; v < 4; v++) {
531
+ const src = (baseIndex + v) * 3
532
+ positions.push(positions[src]!, positions[src + 1]!, positions[src + 2]!)
533
+ normals.push(-dir[0], -dir[1], -dir[2])
534
+ const uvSrc = (baseIndex + v) * 2
535
+ uvs.push(uvs[uvSrc]!, uvs[uvSrc + 1]!)
536
+ colors.push(colors[src]!, colors[src + 1]!, colors[src + 2]!)
537
+ skyLights.push(skyLights[src / 3]!)
538
+ blockLights.push(blockLights[src / 3]!)
539
+ }
540
+ indices.push(
541
+ dupBase,
542
+ dupBase + 2,
543
+ dupBase + 1,
544
+ dupBase + 1,
545
+ dupBase + 2,
546
+ dupBase + 3,
451
547
  )
452
548
  }
453
549
  }
@@ -501,9 +597,19 @@ export function renderWasmOutputToGeometry(
501
597
  const positions: number[] = []
502
598
  const normals: number[] = []
503
599
  const colors: number[] = []
600
+ const skyLights: number[] = []
601
+ const blockLights: number[] = []
504
602
  const uvs: number[] = []
505
603
  const indices: number[] = []
506
604
 
605
+ const blendPositions: number[] = []
606
+ const blendNormals: number[] = []
607
+ const blendColors: number[] = []
608
+ const blendSkyLights: number[] = []
609
+ const blendBlockLights: number[] = []
610
+ const blendUvs: number[] = []
611
+ const blendIndices: number[] = []
612
+
507
613
  const liquidQueue: Array<{
508
614
  pos: Vec3,
509
615
  type: number,
@@ -512,8 +618,6 @@ export function renderWasmOutputToGeometry(
512
618
  isRealWater: boolean,
513
619
  }> = []
514
620
 
515
- let currentIndex = 0
516
-
517
621
  const sectionHeight = options?.sectionHeight ?? SECTION_HEIGHT
518
622
  const shaderCubesEnabled = options?.shaderCubes !== false
519
623
  const [sectionOx, sectionOy, sectionOz] = sectionKey.split(',').map((v) => parseInt(v, 10))
@@ -602,6 +706,15 @@ export function renderWasmOutputToGeometry(
602
706
  const models = cachedModel.models
603
707
  if (!models || models.length == 0) continue
604
708
 
709
+ const routeToBlend = prismBlock.transparent && isSemiTransparentBlockName(cachedModel.blockName)
710
+ const tgtPos = routeToBlend ? blendPositions : positions
711
+ const tgtNorm = routeToBlend ? blendNormals : normals
712
+ const tgtCol = routeToBlend ? blendColors : colors
713
+ const tgtSky = routeToBlend ? blendSkyLights : skyLights
714
+ const tgtBlock = routeToBlend ? blendBlockLights : blockLights
715
+ const tgtUv = routeToBlend ? blendUvs : uvs
716
+ const tgtIdx = routeToBlend ? blendIndices : indices
717
+
605
718
  const faceNameToIndex: Record<string, number> = {
606
719
  'up': 0,
607
720
  'down': 1,
@@ -701,7 +814,9 @@ export function renderWasmOutputToGeometry(
701
814
 
702
815
  const faceDataIndex = faceIdx === undefined ? undefined : wasmFaceToDataIndex[faceIdx]
703
816
  const aoValuesRaw = faceDataIndex === undefined ? undefined : block.ao_data[faceDataIndex]
704
- const lightValuesRaw = faceDataIndex === undefined ? undefined : block.light_data[faceDataIndex]
817
+ const skyValuesRaw = faceDataIndex === undefined ? undefined : block.sky_light_data?.[faceDataIndex]
818
+ const blockValuesRaw = faceDataIndex === undefined ? undefined : block.block_light_data?.[faceDataIndex]
819
+ const lightValuesRaw = faceDataIndex === undefined ? undefined : block.light_data?.[faceDataIndex]
705
820
 
706
821
  const texture = matchingEFace.texture as any
707
822
  const u = texture.u || 0
@@ -718,7 +833,7 @@ export function renderWasmOutputToGeometry(
718
833
 
719
834
  const tint = getTint(matchingEFace, cachedModel.blockName, cachedModel.blockProps, biome, world)
720
835
 
721
- const baseIndex = currentIndex
836
+ const baseIndex = tgtPos.length / 3
722
837
  const computedAoValues = [3, 3, 3, 3]
723
838
  for (let cornerIdx = 0; cornerIdx < 4; cornerIdx++) {
724
839
  const pos = corners[cornerIdx]
@@ -739,20 +854,19 @@ export function renderWasmOutputToGeometry(
739
854
  vertex[2] + (bz & 15) - 8
740
855
  ]
741
856
 
742
- positions.push(...worldPos)
857
+ tgtPos.push(...worldPos)
743
858
 
744
- normals.push(transformedDir[0], transformedDir[1], transformedDir[2])
859
+ tgtNorm.push(transformedDir[0], transformedDir[1], transformedDir[2])
745
860
 
746
- const useModelLighting = !cachedModel.isCube && world
861
+ const useModelLighting = (!cachedModel.isCube || globalMatrix != null) && world
747
862
 
748
863
  let ao = 3
749
- let cornerLightResult = 15
750
- let light: number
864
+ let skyLightNorm = 1
865
+ let blockLightNorm = 0
866
+ const faceDir = transformedDirI as [number, number, number]
751
867
 
752
868
  if (!doAO) {
753
- // JS parity: skip AO/light sampling, emit full-bright vertex.
754
869
  computedAoValues[cornerIdx] = 3
755
- light = 1
756
870
  } else if (useModelLighting) {
757
871
  const cursor = new Vec3(bx, by, bz)
758
872
 
@@ -779,55 +893,45 @@ export function renderWasmOutputToGeometry(
779
893
  ao = (side1Block && side2Block) ? 0 : (3 - (side1Block + side2Block + cornerBlock))
780
894
  computedAoValues[cornerIdx] = ao
781
895
 
782
- const neighborPos = cursor.offset(transformedDirI[0], transformedDirI[1], transformedDirI[2])
783
- const baseLight15 = world.getLight(neighborPos)
784
-
785
- if (world.config.smoothLighting) {
786
- const dirVec = new Vec3(transformedDirI[0], transformedDirI[1], transformedDirI[2])
787
- const getVec = (v: Vec3) => {
788
- for (const coord of ['x', 'y', 'z'] as const) {
789
- if (Math.abs((dirVec as any)[coord]) > 0) (v as any)[coord] = 0
790
- }
791
- return v.plus(dirVec)
792
- }
793
-
794
- const side1LightDir = getVec(new Vec3(side1DirI[0], side1DirI[1], side1DirI[2]))
795
- const side2LightDir = getVec(new Vec3(side2DirI[0], side2DirI[1], side2DirI[2]))
796
- const cornerLightDir = getVec(new Vec3(cornerDirI[0], cornerDirI[1], cornerDirI[2]))
797
-
798
- const side1Light = world.getLight(cursor.plus(side1LightDir))
799
- const side2Light = world.getLight(cursor.plus(side2LightDir))
800
- const cornerLight = world.getLight(cursor.plus(cornerLightDir))
801
-
802
- cornerLightResult = (side1Light + side2Light + cornerLight + baseLight15) / 4
803
- } else {
804
- cornerLightResult = baseLight15
805
- }
896
+ const cornerDirL = matmul3(globalMatrix, [dx, dy, dz])
897
+ const cornerOffsetI: [number, number, number] = [
898
+ Math.round(cornerDirL[0]), Math.round(cornerDirL[1]), Math.round(cornerDirL[2]),
899
+ ]
900
+ const channels = smoothChannelLightAt(
901
+ world,
902
+ cursor,
903
+ faceDir,
904
+ cornerOffsetI,
905
+ faceIdx ?? 0,
906
+ )
907
+ skyLightNorm = channels.sky
908
+ blockLightNorm = channels.block
806
909
  } else {
807
910
  const aoValues = aoValuesRaw ?? [3, 3, 3, 3]
808
- const lightValues = lightValuesRaw ?? [1, 1, 1, 1]
809
911
 
810
912
  ao = aoValues[cornerIdx] ?? 3
811
913
  computedAoValues[cornerIdx] = ao
812
914
 
813
- const baseLight = lightValues[cornerIdx] ?? 1
814
- cornerLightResult = baseLight * 15
815
- }
816
-
817
- if (doAO) {
818
- const faceDir = transformedDirI as [number, number, number]
819
- light = computeMesherVertexLight(world, ao, cornerLightResult, faceDir)
915
+ if (skyValuesRaw && blockValuesRaw) {
916
+ skyLightNorm = skyValuesRaw[cornerIdx] ?? 1
917
+ blockLightNorm = blockValuesRaw[cornerIdx] ?? 0
918
+ } else {
919
+ const combined = lightValuesRaw?.[cornerIdx] ?? 1
920
+ skyLightNorm = combined
921
+ blockLightNorm = 0
922
+ }
820
923
  }
821
924
 
822
- colors.push(tint[0] * light!, tint[1] * light!, tint[2] * light!)
925
+ const tintAo = vertexTintAoColor(world, tint, ao, faceDir)
926
+ tgtCol.push(tintAo[0], tintAo[1], tintAo[2])
927
+ tgtSky.push(skyLightNorm)
928
+ tgtBlock.push(blockLightNorm)
823
929
 
824
930
  const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
825
931
  const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
826
932
  const finalU = baseu * su + u
827
933
  const finalV = basev * sv + v
828
- uvs.push(finalU, finalV)
829
-
830
- currentIndex++
934
+ tgtUv.push(finalU, finalV)
831
935
  }
832
936
 
833
937
  const aoValues = computedAoValues
@@ -840,7 +944,7 @@ export function renderWasmOutputToGeometry(
840
944
  tri1 = [baseIndex, baseIndex + 1, baseIndex + 2]
841
945
  tri2 = [baseIndex + 2, baseIndex + 1, baseIndex + 3]
842
946
  }
843
- indices.push(...tri1, ...tri2)
947
+ tgtIdx.push(...tri1, ...tri2)
844
948
  }
845
949
  }
846
950
  }
@@ -862,11 +966,13 @@ export function renderWasmOutputToGeometry(
862
966
  q.biome,
863
967
  q.water,
864
968
  q.isRealWater,
865
- positions,
866
- normals,
867
- colors,
868
- uvs,
869
- indices,
969
+ blendPositions,
970
+ blendNormals,
971
+ blendColors,
972
+ blendSkyLights,
973
+ blendBlockLights,
974
+ blendUvs,
975
+ blendIndices,
870
976
  )
871
977
  }
872
978
  }
@@ -880,9 +986,22 @@ export function renderWasmOutputToGeometry(
880
986
  positions,
881
987
  normals,
882
988
  colors,
989
+ skyLights,
990
+ blockLights,
883
991
  uvs,
884
992
  indices,
885
993
  },
994
+ ...(blendPositions.length > 0 ? {
995
+ blendGeometry: {
996
+ positions: blendPositions,
997
+ normals: blendNormals,
998
+ colors: blendColors,
999
+ skyLights: blendSkyLights,
1000
+ blockLights: blendBlockLights,
1001
+ uvs: blendUvs,
1002
+ indices: blendIndices,
1003
+ },
1004
+ } : {}),
886
1005
  ...(shaderCubes ? { shaderCubes } : {}),
887
1006
  }
888
1007
 
@@ -1017,12 +1136,17 @@ export function mesherGeometryToExportFormat(
1017
1136
  mesherGeometry: MesherGeometryOutput,
1018
1137
  version: string,
1019
1138
  cameraPosition = { x: 0, y: 0, z: 0 },
1020
- cameraRotation = { pitch: 0, yaw: 0 }
1139
+ cameraRotation = { pitch: 0, yaw: 0 },
1140
+ skyLevel = 1,
1021
1141
  ): ExportedWorldGeometry {
1022
- // Convert typed arrays to regular number arrays
1023
1142
  const positions = Array.from(mesherGeometry.positions) as number[]
1024
1143
  const normals = mesherGeometry.normals ? (Array.from(mesherGeometry.normals) as number[]) : []
1025
- const colors = mesherGeometry.colors ? (Array.from(mesherGeometry.colors) as number[]) : []
1144
+ const tintColors = mesherGeometry.colors ? (Array.from(mesherGeometry.colors) as number[]) : []
1145
+ const skyLights = mesherGeometry.skyLights ? (Array.from(mesherGeometry.skyLights) as number[]) : []
1146
+ const blockLights = mesherGeometry.blockLights ? (Array.from(mesherGeometry.blockLights) as number[]) : []
1147
+ const colors = skyLights.length
1148
+ ? bakeLegacyVertexColors(tintColors, skyLights, blockLights, skyLevel)
1149
+ : tintColors
1026
1150
  const uvs = mesherGeometry.uvs ? (Array.from(mesherGeometry.uvs) as number[]) : []
1027
1151
  const indices = Array.from(mesherGeometry.indices) as number[]
1028
1152
 
@@ -1043,9 +1167,27 @@ export function mesherGeometryToExportFormat(
1043
1167
  positions,
1044
1168
  normals,
1045
1169
  colors,
1170
+ skyLights,
1171
+ blockLights,
1046
1172
  uvs,
1047
1173
  indices,
1048
1174
  },
1175
+ ...(mesherGeometry.blend ? {
1176
+ blendGeometry: {
1177
+ positions: Array.from(mesherGeometry.blend.positions),
1178
+ normals: Array.from(mesherGeometry.blend.normals),
1179
+ colors: bakeLegacyVertexColors(
1180
+ Array.from(mesherGeometry.blend.colors),
1181
+ Array.from(mesherGeometry.blend.skyLights),
1182
+ Array.from(mesherGeometry.blend.blockLights),
1183
+ skyLevel,
1184
+ ),
1185
+ skyLights: Array.from(mesherGeometry.blend.skyLights),
1186
+ blockLights: Array.from(mesherGeometry.blend.blockLights),
1187
+ uvs: Array.from(mesherGeometry.blend.uvs),
1188
+ indices: Array.from(mesherGeometry.blend.indices),
1189
+ },
1190
+ } : {}),
1049
1191
  }
1050
1192
 
1051
1193
  return {
@@ -59,7 +59,11 @@ export interface ShaderCubeBlockInput {
59
59
  position: [number, number, number]
60
60
  visible_faces: number
61
61
  ao_data: number[][]
62
- light_data: number[][]
62
+ /** @deprecated combined f32; prefer sky_light_data + block_light_data or light_combined */
63
+ light_data?: number[][]
64
+ sky_light_data?: number[][]
65
+ block_light_data?: number[][]
66
+ /** Per-corner nibble-packed byte: high=sky4, low=block4 */
63
67
  light_combined?: number[][]
64
68
  }
65
69
 
@@ -286,6 +290,12 @@ export function unpackTexIndexFromWord2(word2: number): number {
286
290
  return word2 & ((1 << WORD2.TEX_INDEX_BITS) - 1)
287
291
  }
288
292
 
293
+ function packCornerLightByte (skyNorm: number, blockNorm: number): number {
294
+ const sky4 = Math.min(15, Math.round(Math.max(0, skyNorm) * 15))
295
+ const block4 = Math.min(15, Math.round(Math.max(0, blockNorm) * 15))
296
+ return ((sky4 << 4) | block4) & 0xff
297
+ }
298
+
289
299
  function lightCombinedForFace(
290
300
  block: ShaderCubeBlockInput,
291
301
  faceDataIndex: number,
@@ -294,11 +304,13 @@ function lightCombinedForFace(
294
304
  if (packed && packed.length === 4) {
295
305
  return packed
296
306
  }
297
- const floats = block.light_data[faceDataIndex] ?? [1, 1, 1, 1]
298
- return floats.map((f) => {
299
- if (f >= 1) return 255
300
- return Math.min(255, Math.round(f * 255))
301
- })
307
+ const sky = block.sky_light_data?.[faceDataIndex]
308
+ const blockL = block.block_light_data?.[faceDataIndex]
309
+ if (sky && blockL && sky.length === 4 && blockL.length === 4) {
310
+ return sky.map((s, i) => packCornerLightByte(s ?? 1, blockL[i] ?? 0))
311
+ }
312
+ const floats = block.light_data?.[faceDataIndex] ?? [1, 1, 1, 1]
313
+ return floats.map((f) => packCornerLightByte(f, 0))
302
314
  }
303
315
 
304
316
  function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number> {
@@ -5,7 +5,9 @@ import {
5
5
  raycastAabb,
6
6
  raycastShaderBlocksAabb,
7
7
  raycastSectionAabb,
8
+ sectionAabbIntersectsRay,
8
9
  } from '../../three/sectionRaycastAabb'
10
+ import { LEGACY_SECTION_HALF_EXTENT } from '../../three/legacySectionCull'
9
11
  import { SHADER_CUBES_WORDS_PER_FACE } from '../bridge/shaderCubeBridge'
10
12
  import { WORD0 } from '../../three/shaders/cubeBlockShader'
11
13
 
@@ -103,6 +105,24 @@ test('raycastShaderBlocksAabb: SoA stride-1 layout (GlobalBlockBuffer style)', (
103
105
  expect(t).toBeLessThan(4)
104
106
  })
105
107
 
108
+ const SECTION_HALF = LEGACY_SECTION_HALF_EXTENT + 0.01
109
+
110
+ test('sectionAabbIntersectsRay: ray toward section center within far', () => {
111
+ expect(sectionAabbIntersectsRay(8, 8, 8, 4, 8, 8, 1, 0, 0, 4, SECTION_HALF)).toBe(true)
112
+ })
113
+
114
+ test('sectionAabbIntersectsRay: far shorter than gap to section', () => {
115
+ expect(sectionAabbIntersectsRay(8, 8, 8, -20, 8, 8, 1, 0, 0, 3, SECTION_HALF)).toBe(false)
116
+ })
117
+
118
+ test('sectionAabbIntersectsRay: parallel offset ray misses box', () => {
119
+ expect(sectionAabbIntersectsRay(8, 8, 8, -20, 8, -20, 1, 0, 0, 100, SECTION_HALF)).toBe(false)
120
+ })
121
+
122
+ test('sectionAabbIntersectsRay: origin inside box', () => {
123
+ expect(sectionAabbIntersectsRay(8, 8, 8, 8, 8, 8, 0, 1, 0, 4, SECTION_HALF)).toBe(true)
124
+ })
125
+
106
126
  test('raycastAabb: narrow floor slab blocks downward ray', () => {
107
127
  const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE * 2)
108
128
  words[0] = 4 | (0 << WORD0.LY_SHIFT) | (4 << WORD0.LZ_SHIFT)
@@ -16,7 +16,8 @@ import {
16
16
  SHADER_CUBES_WORDS_PER_FACE,
17
17
  } from '../bridge/shaderCubeBridge'
18
18
  import { GlobalBlockBuffer } from '../../three/globalBlockBuffer'
19
- import { createCubeBlockMaterial } from '../../three/shaders/cubeBlockShader'
19
+ import { buildVisibleCubeSpans } from '../../three/cubeDrawSpans'
20
+ import { createCubeBlockMaterial, computeSectionOriginRel } from '../../three/shaders/cubeBlockShader'
20
21
  import * as THREE from 'three'
21
22
  import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
22
23
 
@@ -349,6 +350,38 @@ test('packWord2Empty: bit 18 set regardless of high X/Z bits in word2', () => {
349
350
  expect(withHighBits & (1 << WORD2.EMPTY_SHIFT)).not.toBe(0)
350
351
  })
351
352
 
353
+ test('section index relative decode past 2^20: exact integer subtract', () => {
354
+ const sectionBlockX = 21_050_000
355
+ const renderOrigin = { x: 21_000_000, y: 0, z: 0 }
356
+ const words: number[] = []
357
+ const block = {
358
+ position: [0, 0, 0] as [number, number, number],
359
+ visible_faces: 1 << 2,
360
+ ao_data: [[3, 3, 3, 3]],
361
+ light_data: [[1, 1, 1, 1]],
362
+ light_combined: [[255, 255, 255, 255]],
363
+ }
364
+ const { textureIndexMapping, tintPalette } = getShaderCubeResources()
365
+ const model = { elements: [{ faces: SIX_FACE_TEXTURES }] }
366
+ tryBuildShaderCubeInstances(
367
+ block,
368
+ { blockName: 'stone', blockProps: {}, isCube: true, model },
369
+ model,
370
+ {
371
+ sectionOrigin: { x: sectionBlockX, y: 0, z: 0 },
372
+ sectionHeight: 16,
373
+ tintPalette,
374
+ textureIndexMapping,
375
+ },
376
+ words,
377
+ )
378
+ const base = decodeSectionBaseFromWords(words[2]!, words[3]!)
379
+ const sX = base.x / 16
380
+ const sectionOriginRel = computeSectionOriginRel(renderOrigin)
381
+ const sXr = sX - sectionOriginRel.x
382
+ expect(sXr * 16).toBe(sectionBlockX - renderOrigin.x)
383
+ })
384
+
352
385
  test('GlobalBlockBuffer: free-list reuses slot with EMPTY sentinel', () => {
353
386
  const scene = new THREE.Scene()
354
387
  const mat = createCubeBlockMaterial()
@@ -661,6 +694,35 @@ test('GlobalBlockBuffer: takeSectionData reads relocated section slot', () => {
661
694
  mat.dispose()
662
695
  })
663
696
 
697
+ test('GlobalBlockBuffer: pendingMove draw start uses oldStart for visible spans', () => {
698
+ const scene = new THREE.Scene()
699
+ const mat = createCubeBlockMaterial()
700
+ const buffer = new GlobalBlockBuffer(mat, scene)
701
+
702
+ buffer.addSection('a', makeSectionWords([10]), 1)
703
+ buffer.addSection('b', makeSectionWords([20]), 1)
704
+ buffer.addSection('c', makeSectionWords([30]), 1)
705
+ buffer.removeSection('b')
706
+ drainAllUploads(buffer)
707
+
708
+ buffer.compactStep()
709
+ const move = buffer.getPendingMove()
710
+ expect(move?.key).toBe('c')
711
+
712
+ const slotStart = buffer.getSectionSlot('c')!.start
713
+ expect(buffer.getSectionDrawStart('c')).toBe(move!.oldStart)
714
+ expect(buffer.getSectionDrawStart('c')).not.toBe(slotStart)
715
+
716
+ const spans = buildVisibleCubeSpans(
717
+ [{ start: buffer.getSectionDrawStart('c')!, count: 1 }],
718
+ buffer.getHighWatermark(),
719
+ )
720
+ expect(spans[0]?.start).toBe(move!.oldStart)
721
+
722
+ buffer.dispose()
723
+ mat.dispose()
724
+ })
725
+
664
726
  test('GlobalBlockBuffer: uploadDirtyRange budgets large dirty span across frames', () => {
665
727
  const scene = new THREE.Scene()
666
728
  const mat = createCubeBlockMaterial()
@@ -679,12 +741,12 @@ test('GlobalBlockBuffer: uploadDirtyRange budgets large dirty span across frames
679
741
 
680
742
  const w0Attr = buffer.mesh.geometry.getAttribute('a_w0') as THREE.InstancedBufferAttribute
681
743
  buffer.uploadDirtyRange()
682
- expect(w0Attr.updateRange.offset).toBe(0)
683
- expect(w0Attr.updateRange.count).toBe(15_000)
744
+ expect(w0Attr.updateRanges[0].start).toBe(0)
745
+ expect(w0Attr.updateRanges[0].count).toBe(15_000)
684
746
 
685
747
  buffer.uploadDirtyRange()
686
- expect(w0Attr.updateRange.offset).toBe(15_000)
687
- expect(w0Attr.updateRange.count).toBe(5_000)
748
+ expect(w0Attr.updateRanges[0].start).toBe(15_000)
749
+ expect(w0Attr.updateRanges[0].count).toBe(5_000)
688
750
 
689
751
  buffer.uploadDirtyRange()
690
752
  expect((buffer as unknown as { pendingRanges: unknown[] }).pendingRanges).toHaveLength(0)