minecraft-renderer 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/mesher.js +1 -1
  2. package/dist/mesher.js.map +2 -2
  3. package/dist/mesherWasm.js +3740 -183
  4. package/dist/minecraft-renderer.js +332 -60
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +705 -433
  7. package/package.json +1 -1
  8. package/src/graphicsBackend/config.ts +4 -0
  9. package/src/graphicsBackend/playerState.ts +1 -0
  10. package/src/graphicsBackend/rendererOptionsSync.ts +1 -0
  11. package/src/lib/worldrendererCommon.ts +13 -0
  12. package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
  13. package/src/mesher-shared/shared.ts +8 -0
  14. package/src/playerState/playerState.ts +2 -0
  15. package/src/three/chunkMeshManager.ts +312 -39
  16. package/src/three/globalBlockBuffer.ts +292 -0
  17. package/src/three/menuBackground/config.ts +1 -1
  18. package/src/three/menuBackground/defaultOptions.ts +27 -19
  19. package/src/three/modules/sciFiWorldReveal.ts +162 -68
  20. package/src/three/modules/starfield.ts +9 -1
  21. package/src/three/sectionRaycastAabb.ts +167 -0
  22. package/src/three/shaderCubeMesh.ts +93 -0
  23. package/src/three/shaders/cubeBlockShader.ts +354 -0
  24. package/src/three/shaders/textureIndexMapping.ts +122 -0
  25. package/src/three/shaders/tintPalette.ts +198 -0
  26. package/src/three/worldGeometryExport.ts +53 -25
  27. package/src/three/worldRendererThree.ts +93 -26
  28. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  29. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +396 -0
  30. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  31. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
  32. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
  33. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
  34. package/src/wasm-mesher/worker/mesherWasm.ts +17 -2
@@ -2,6 +2,9 @@
2
2
  import * as THREE from 'three'
3
3
  import type { WorldRendererThree } from './worldRendererThree'
4
4
  import type { ExportedSection, ExportedWorldGeometry } from '../mesher-shared/exportedGeometryTypes'
5
+ import { getShaderCubeResources } from '../wasm-mesher/bridge/shaderCubeBridge'
6
+ import { createCubeBlockMaterial } from './shaders/cubeBlockShader'
7
+ import { createShaderCubeMesh } from './shaderCubeMesh'
5
8
 
6
9
  export type { ExportedSection, ExportedWorldGeometry } from '../mesher-shared/exportedGeometryTypes'
7
10
 
@@ -114,40 +117,62 @@ export async function loadWorldGeometryFromUrl(url: string): Promise<ExportedWor
114
117
  * Recreate THREE.js meshes from exported geometry
115
118
  * Returns an array of mesh groups that can be added to a scene
116
119
  */
120
+ function shaderMaterialForExport(legacyMaterial: THREE.Material): THREE.ShaderMaterial | null {
121
+ const atlas = (legacyMaterial as THREE.MeshBasicMaterial).map
122
+ ?? (legacyMaterial as THREE.MeshLambertMaterial).map
123
+ if (!atlas) return null
124
+ const shaderMat = createCubeBlockMaterial()
125
+ shaderMat.uniforms.u_atlas.value = atlas
126
+ const { tintPalette } = getShaderCubeResources()
127
+ if (!tintPalette.isReady()) tintPalette.createTexture()
128
+ shaderMat.uniforms.u_tintPalette.value = tintPalette.getTexture()
129
+ return shaderMat
130
+ }
131
+
117
132
  export function createMeshesFromExport(
118
133
  exportData: ExportedWorldGeometry,
119
- material: THREE.Material
134
+ material: THREE.Material,
135
+ shaderMaterial?: THREE.ShaderMaterial | null,
120
136
  ): THREE.Group[] {
121
137
  const groups: THREE.Group[] = []
138
+ const resolvedShaderMat = shaderMaterial ?? shaderMaterialForExport(material)
122
139
 
123
140
  for (const section of exportData.sections) {
124
- const geometry = new THREE.BufferGeometry()
141
+ const group = new THREE.Group()
142
+ group.name = 'chunk'
125
143
 
126
- geometry.setAttribute('position', new THREE.Float32BufferAttribute(section.geometry.positions, 3))
127
- if (section.geometry.normals.length) {
128
- geometry.setAttribute('normal', new THREE.Float32BufferAttribute(section.geometry.normals, 3))
129
- }
130
- if (section.geometry.colors.length) {
131
- geometry.setAttribute('color', new THREE.Float32BufferAttribute(section.geometry.colors, 3))
132
- }
133
- if (section.geometry.uvs.length) {
134
- geometry.setAttribute('uv', new THREE.Float32BufferAttribute(section.geometry.uvs, 2))
144
+ const hasLegacy = section.geometry.positions.length > 0 && section.geometry.indices.length > 0
145
+ if (hasLegacy) {
146
+ const geometry = new THREE.BufferGeometry()
147
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(section.geometry.positions, 3))
148
+ if (section.geometry.normals.length) {
149
+ geometry.setAttribute('normal', new THREE.Float32BufferAttribute(section.geometry.normals, 3))
150
+ }
151
+ if (section.geometry.colors.length) {
152
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(section.geometry.colors, 3))
153
+ }
154
+ if (section.geometry.uvs.length) {
155
+ geometry.setAttribute('uv', new THREE.Float32BufferAttribute(section.geometry.uvs, 2))
156
+ }
157
+ const maxIndex = Math.max(...section.geometry.indices)
158
+ const IndexArrayType = maxIndex > 65_535 ? Uint32Array : Uint16Array
159
+ geometry.setIndex(new THREE.BufferAttribute(new IndexArrayType(section.geometry.indices), 1))
160
+ const mesh = new THREE.Mesh(geometry, material)
161
+ mesh.position.set(section.position.x, section.position.y, section.position.z)
162
+ mesh.name = 'mesh'
163
+ group.add(mesh)
135
164
  }
136
165
 
137
- // Use appropriate index type based on vertex count
138
- const maxIndex = Math.max(...section.geometry.indices)
139
- const IndexArrayType = maxIndex > 65_535 ? Uint32Array : Uint16Array
140
- geometry.setIndex(new THREE.BufferAttribute(new IndexArrayType(section.geometry.indices), 1))
141
-
142
- const mesh = new THREE.Mesh(geometry, material)
143
- mesh.position.set(section.position.x, section.position.y, section.position.z)
144
- mesh.name = 'mesh'
145
-
146
- const group = new THREE.Group()
147
- group.name = 'chunk'
148
- group.add(mesh)
166
+ const shaderCubes = section.shaderCubes
167
+ if (shaderCubes && shaderCubes.count > 0 && resolvedShaderMat) {
168
+ const shaderMesh = createShaderCubeMesh(shaderCubes, resolvedShaderMat)
169
+ shaderMesh.position.set(section.position.x, section.position.y, section.position.z)
170
+ group.add(shaderMesh)
171
+ }
149
172
 
150
- groups.push(group)
173
+ if (group.children.length > 0) {
174
+ groups.push(group)
175
+ }
151
176
  }
152
177
 
153
178
  return groups
@@ -222,7 +247,10 @@ export async function applyWorldGeometryExport(
222
247
  material = rendererMaterial
223
248
  }
224
249
 
225
- const groups = createMeshesFromExport(exportData, material)
250
+ const shaderMat = exportData.sections.some(s => (s.shaderCubes?.count ?? 0) > 0)
251
+ ? shaderMaterialForExport(material)
252
+ : null
253
+ const groups = createMeshesFromExport(exportData, material, shaderMat)
226
254
  const container = new THREE.Group()
227
255
  container.name = GEOMETRY_EXPORT_GROUP_NAME
228
256
  if (hasEmbeddedTexture) {
@@ -42,6 +42,11 @@ type SectionKey = string
42
42
 
43
43
  export class WorldRendererThree extends WorldRendererCommon {
44
44
  outputFormat = 'threeJs' as const
45
+
46
+ protected override isShaderCubeBlocksEnabled(): boolean {
47
+ return this.worldRendererConfig.shaderCubeBlocks === true
48
+ && !!this.renderer?.capabilities?.isWebGL2
49
+ }
45
50
  chunkMeshManager: ChunkMeshManager
46
51
  get sectionObjects() {
47
52
  return this.chunkMeshManager.sectionObjects
@@ -65,6 +70,9 @@ export class WorldRendererThree extends WorldRendererCommon {
65
70
  cursorBlock: CursorBlock
66
71
  onRender: Array<(deltaTime: number) => void> = []
67
72
  private lastRenderTime = 0
73
+ private animatedFov = 0
74
+ private lastFovAnimTime = 0
75
+ private static readonly FOV_TRANSITION_MS = 200
68
76
  cameraShake: CameraShake
69
77
  cameraContainer!: THREE.Object3D
70
78
  media: ThreeJsMedia
@@ -519,6 +527,12 @@ export class WorldRendererThree extends WorldRendererCommon {
519
527
  this.onReactiveConfigUpdated('defaultSkybox', (value) => {
520
528
  this.skyboxRenderer.updateDefaultSkybox(value)
521
529
  })
530
+ this.onReactiveConfigUpdated('shaderCubeDebugMode', () => {
531
+ this.chunkMeshManager.syncCubeShaderUniforms()
532
+ })
533
+ this.onReactiveConfigUpdated('futuristicReveal', () => {
534
+ this.updateModulesFromConfig()
535
+ })
522
536
 
523
537
  let currentHandRenderer = this.displayOptions.inWorldRenderingConfig.handRenderer
524
538
  this.onReactiveConfigUpdated('handRenderer', (value) => {
@@ -615,6 +629,7 @@ export class WorldRendererThree extends WorldRendererCommon {
615
629
  texture.needsUpdate = true
616
630
  texture.flipY = false
617
631
  this.material.map = texture
632
+ this.chunkMeshManager.syncCubeShaderUniforms()
618
633
 
619
634
  const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage!)
620
635
  itemsTexture.needsUpdate = true
@@ -731,12 +746,21 @@ export class WorldRendererThree extends WorldRendererCommon {
731
746
  * Optionally update data that are depedendent on the viewer position
732
747
  */
733
748
  updatePosDataChunk(key: string) {
749
+ const sectionObj = this.sectionObjects[key]
750
+ if (!sectionObj) return
751
+
734
752
  const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
735
753
  // sum of distances: x + y + z
736
754
  const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
737
- const sectionObj = this.sectionObjects[key]
738
- const section = (sectionObj as any).mesh ?? sectionObj.children.find(child => child.name === 'mesh')!
739
- section.renderOrder = 500 - chunkDistance
755
+ const renderOrder = 500 - chunkDistance
756
+
757
+ // Cubes in globalBlockBuffer may leave sectionObj.mesh unset.
758
+ const drawable = sectionObj.mesh
759
+ ?? sectionObj.shaderMesh
760
+ ?? sectionObj.children.find(child => child.name === 'mesh' || child.name === 'shaderMesh')
761
+ if (drawable) {
762
+ drawable.renderOrder = renderOrder
763
+ }
740
764
  }
741
765
 
742
766
  override updateViewerPosition(pos: Vec3): void {
@@ -817,7 +841,7 @@ export class WorldRendererThree extends WorldRendererCommon {
817
841
  continue
818
842
  }
819
843
 
820
- if (!update.geometry.positions.length) {
844
+ if (!this.chunkMeshManager.sectionHasRenderableContent(update.geometry)) {
821
845
  this.chunkMeshManager.releaseSection(update.key)
822
846
  continue
823
847
  }
@@ -856,7 +880,7 @@ export class WorldRendererThree extends WorldRendererCommon {
856
880
  return
857
881
  }
858
882
 
859
- if (!data.geometry.positions.length) {
883
+ if (!this.chunkMeshManager.sectionHasRenderableContent(data.geometry)) {
860
884
  this.chunkMeshManager.releaseSection(data.key)
861
885
  return
862
886
  }
@@ -962,25 +986,22 @@ export class WorldRendererThree extends WorldRendererCommon {
962
986
  raycaster.set(scenePos, direction)
963
987
  raycaster.far = distance // Limit raycast distance
964
988
 
965
- // Filter to only nearby chunks for performance
966
- const nearbyChunks = Object.values(this.sectionObjects)
967
- .filter(obj => obj.name === 'chunk' && obj.visible)
968
- .filter(obj => {
969
- // Get the mesh child which has the actual geometry
970
- const mesh = obj.children.find(child => child.name === 'mesh')
971
- if (!mesh) return false
972
-
973
- // Check distance from player position to chunk
974
- const chunkWorldPos = this._tpChunkWorldPos
975
- mesh.getWorldPosition(chunkWorldPos)
976
- const distance = scenePos.distanceTo(chunkWorldPos)
977
- return distance < 80 // Only check chunks within 80 blocks
978
- })
989
+ const maxCenterDistance = 80
990
+ const maxCenterDistSq = maxCenterDistance * maxCenterDistance
991
+ const ox = pos.x
992
+ const oy = pos.y
993
+ const oz = pos.z
979
994
 
980
- // Get all mesh children for raycasting
995
+ // Legacy / deferred-shader meshes (scene-relative raycast)
981
996
  const meshes: THREE.Object3D[] = []
982
- for (const chunk of nearbyChunks) {
983
- const mesh = chunk.children.find(child => child.name === 'mesh')
997
+ for (const obj of Object.values(this.sectionObjects)) {
998
+ if (obj.name !== 'chunk' || !obj.visible) continue
999
+ if (obj.worldX === undefined) continue
1000
+ const dcx = obj.worldX - ox
1001
+ const dcy = obj.worldY! - oy
1002
+ const dcz = obj.worldZ! - oz
1003
+ if (dcx * dcx + dcy * dcy + dcz * dcz > maxCenterDistSq) continue
1004
+ const mesh = obj.children.find(child => child.name === 'mesh' || child.name === 'shaderMesh')
984
1005
  if (mesh) meshes.push(mesh)
985
1006
  }
986
1007
 
@@ -988,10 +1009,20 @@ export class WorldRendererThree extends WorldRendererCommon {
988
1009
 
989
1010
  let finalDistance = distance
990
1011
  if (intersects.length > 0) {
991
- // Use intersection distance minus a small offset to prevent clipping
992
1012
  finalDistance = Math.max(0.5, intersects[0].distance - 0.2)
993
1013
  }
994
1014
 
1015
+ // Shader cubes in global buffer: tight per-section AABBs (no mesh raycast).
1016
+ const boxHit = this.chunkMeshManager.raycastShaderSectionAABBs(
1017
+ pos,
1018
+ direction,
1019
+ finalDistance,
1020
+ maxCenterDistance,
1021
+ )
1022
+ if (boxHit !== undefined) {
1023
+ finalDistance = Math.max(0.5, boxHit - 0.2)
1024
+ }
1025
+
995
1026
  const finalPos = new Vec3(
996
1027
  pos.x + direction.x * finalDistance,
997
1028
  pos.y + direction.y * finalDistance,
@@ -1055,10 +1086,42 @@ export class WorldRendererThree extends WorldRendererCommon {
1055
1086
  }
1056
1087
 
1057
1088
  setCinimaticFov(fov: number): void {
1089
+ this.animatedFov = fov
1058
1090
  this.camera.fov = fov
1059
1091
  this.camera.updateProjectionMatrix()
1060
1092
  }
1061
1093
 
1094
+ private updateSmoothFov(): void {
1095
+ if (this.cinimaticScript.running) return
1096
+
1097
+ const baseFov = this.displayOptions.inWorldRenderingConfig.fov
1098
+ const mult = this.playerStateReactive.fovMultiplier
1099
+ const targetFov = baseFov * (Number.isFinite(mult) ? mult : 1)
1100
+
1101
+ const now = performance.now()
1102
+ if (this.animatedFov === 0) {
1103
+ this.animatedFov = targetFov
1104
+ }
1105
+
1106
+ if (Math.abs(this.animatedFov - targetFov) >= 0.01) {
1107
+ const elapsed = now - this.lastFovAnimTime
1108
+ const progress = Math.min(elapsed / WorldRendererThree.FOV_TRANSITION_MS, 1)
1109
+ const easeOutCubic = (t: number) => 1 - (1 - t) ** 3
1110
+ this.animatedFov += (targetFov - this.animatedFov) * easeOutCubic(progress)
1111
+ if (Math.abs(this.animatedFov - targetFov) < 0.01) {
1112
+ this.animatedFov = targetFov
1113
+ }
1114
+ } else {
1115
+ this.animatedFov = targetFov
1116
+ }
1117
+ this.lastFovAnimTime = now
1118
+
1119
+ if (this.camera.fov !== this.animatedFov) {
1120
+ this.camera.fov = this.animatedFov
1121
+ this.camera.updateProjectionMatrix()
1122
+ }
1123
+ }
1124
+
1062
1125
  updateCamera(pos: Vec3 | null, yaw: number, pitch: number): void {
1063
1126
  // Skip position/rotation updates if cinematic script is running
1064
1127
  if (this.cinimaticScript.running) {
@@ -1200,11 +1263,10 @@ export class WorldRendererThree extends WorldRendererCommon {
1200
1263
  const cameraPos = this.getCameraPosition()
1201
1264
  this.skyboxRenderer.update(cameraPos, this.viewDistance)
1202
1265
 
1203
- const sizeOrFovChanged = sizeChanged || this.displayOptions.inWorldRenderingConfig.fov !== this.camera.fov
1204
- if (sizeOrFovChanged) {
1266
+ this.updateSmoothFov()
1267
+ if (sizeChanged) {
1205
1268
  const size = this.renderer.getSize(new THREE.Vector2())
1206
1269
  this.camera.aspect = size.width / size.height
1207
- this.camera.fov = this.displayOptions.inWorldRenderingConfig.fov
1208
1270
  this.camera.updateProjectionMatrix()
1209
1271
  }
1210
1272
 
@@ -1218,6 +1280,11 @@ export class WorldRendererThree extends WorldRendererCommon {
1218
1280
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
1219
1281
  const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
1220
1282
  this.applyPendingSectionUpdates()
1283
+ const globalBuffer = this.chunkMeshManager.globalBlockBuffer
1284
+ if (globalBuffer) {
1285
+ globalBuffer.setCameraOrigin(this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
1286
+ globalBuffer.uploadDirtyRange()
1287
+ }
1221
1288
  this.renderer.render(this.scene, cam)
1222
1289
 
1223
1290
  if (
@@ -12,8 +12,14 @@ 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 { SECTION_HEIGHT } from '../../mesher-shared/shared'
15
16
  import type { World } from '../../mesher-shared/world'
16
17
  import { resolveBlockPropertiesForMeshing } from '../../mesher-shared/blockPropertiesForMeshing'
18
+ import {
19
+ buildShaderCubesFromWords,
20
+ getShaderCubeResources,
21
+ tryBuildShaderCubeInstances,
22
+ } from './shaderCubeBridge'
17
23
  import { getSideShading, vertexLightFromAo } from '../../mesher-shared/vertexShading'
18
24
  import tintsJson from 'minecraft-data/minecraft-data/data/pc/1.16.2/tints.json'
19
25
 
@@ -82,6 +88,7 @@ interface WasmBlockFaceData {
82
88
  visible_faces: number
83
89
  ao_data: number[][]
84
90
  light_data: number[][]
91
+ light_combined?: number[][] // Packed combined light for shader path ([u8; 4] per visible face)
85
92
  }
86
93
 
87
94
  export interface WasmGeometryOutput {
@@ -445,6 +452,16 @@ const renderLiquidToGeometry = (
445
452
  }
446
453
  }
447
454
 
455
+ export type RenderWasmOptions = {
456
+ /** Section height in blocks. Shader-cube path requires 16; other values keep legacy. */
457
+ sectionHeight?: number
458
+ /**
459
+ * Pack full-cube blocks into instanced shader words.
460
+ * Set false in parity tests that expect legacy vertex buffers only.
461
+ */
462
+ shaderCubes?: boolean
463
+ }
464
+
448
465
  /**
449
466
  * Render WASM mesher output to Three.js geometry
450
467
  */
@@ -453,7 +470,8 @@ export function renderWasmOutputToGeometry(
453
470
  version: string,
454
471
  sectionKey: string,
455
472
  sectionPosition: { x: number, y: number, z: number },
456
- world?: World
473
+ world?: World,
474
+ options?: RenderWasmOptions,
457
475
  ): ExportedSection {
458
476
  const DEBUG = false
459
477
  const log = (...args) => {
@@ -496,6 +514,12 @@ export function renderWasmOutputToGeometry(
496
514
 
497
515
  let currentIndex = 0
498
516
 
517
+ const sectionHeight = options?.sectionHeight ?? SECTION_HEIGHT
518
+ const shaderCubesEnabled = options?.shaderCubes !== false
519
+ const [sectionOx, sectionOy, sectionOz] = sectionKey.split(',').map((v) => parseInt(v, 10))
520
+ const shaderWordBuffer: number[] = []
521
+ const shaderResources = shaderCubesEnabled ? getShaderCubeResources() : null
522
+
499
523
  for (const block of wasmOutput.blocks) {
500
524
  const [bx, by, bz] = block.position
501
525
  const blockStateId = block.block_state_id
@@ -548,185 +572,32 @@ export function renderWasmOutputToGeometry(
548
572
  )
549
573
  if (!cachedModel) continue
550
574
 
551
- if (false) {
552
- // For now, use first model variant (can be extended later)
553
- const modelVariant = cachedModel!.modelVariants[0]
554
- if (!modelVariant) continue
555
-
556
- const { model, globalMatrix, globalShift, elements } = modelVariant
557
-
558
- // Get biome for tint calculation if world is provided
559
- let biome: string | undefined
560
- if (world) {
561
- const blockObj = world!.getBlock(new Vec3(bx, by, bz))
562
- biome = blockObj?.biome?.name
563
- }
564
-
565
- // Process faces in the same order as TypeScript (iterate through model's faces)
566
- // TypeScript uses: for (const face in element.faces)
567
- // We need to match this order to get the same vertex ordering
568
-
569
- // Find the element that contains faces (use cached element data)
570
- const faceElements = elements.filter(elemData => elemData.element.faces && Object.keys(elemData.element.faces).length > 0)
571
-
572
- if (faceElements.length === 0) continue
573
-
574
- // Map face names to their index in WASM output
575
- const faceNameToIndex: Record<string, number> = {
576
- 'up': 0,
577
- 'down': 1,
578
- 'east': 2,
579
- 'west': 3,
580
- 'south': 4,
581
- 'north': 5
582
- }
583
-
584
- // WASM processes faces in fixed order: [up, down, east, west, south, north]
585
- // Build a mapping from WASM face order to data index
586
- const wasmFaceOrder = ['up', 'down', 'east', 'west', 'south', 'north']
587
- const wasmFaceToDataIndex: Record<string, number> = {}
588
- let dataIndex = 0
589
- for (const faceName of wasmFaceOrder) {
590
- const faceIdx = faceNameToIndex[faceName]
591
- if ((block.visible_faces & (1 << faceIdx)) !== 0) {
592
- wasmFaceToDataIndex[faceName] = dataIndex++
593
- }
594
- }
595
-
596
- // Process faces in the order they appear in the model (matching TS)
597
- for (const elemData of faceElements) {
598
- const element = elemData.element
599
- const localMatrix = elemData.localMatrix
600
- const localShift = elemData.localShift
601
-
602
- // eslint-disable-next-line guard-for-in
603
- for (const faceName in element.faces) {
604
- const faceIdx = faceNameToIndex[faceName]
605
- if (faceIdx === undefined) continue
606
-
607
- // Check if this face is visible in WASM output
608
- if ((block.visible_faces & (1 << faceIdx)) === 0) {
609
- continue
610
- }
611
-
612
- const matchingEFace = element.faces[faceName]
613
- const { dir, corners, mask1, mask2 } = elemFaces[faceName]
614
-
615
- // Get the correct data index for this face based on WASM's processing order
616
- const faceDataIndex = wasmFaceToDataIndex[faceName]
617
- if (faceDataIndex === undefined) continue
618
-
619
- const aoValues = block.ao_data[faceDataIndex]
620
- const lightValues = block.light_data[faceDataIndex]
621
-
622
- log(`[WASM] Face ${faceIdx} (${faceName}): dir=[${dir.join(',')}], ao=[${aoValues.join(',')}], light=[${lightValues.map(l => l.toFixed(3)).join(',')}]`)
623
-
624
- const texture = matchingEFace.texture as any
625
- const u = texture.u || 0
626
- const v = texture.v || 0
627
- const su = texture.su || 1
628
- const sv = texture.sv || 1
629
-
630
- // UV rotation (matching reference implementation)
631
- let r = matchingEFace.rotation || 0
632
- if (faceName === 'down') {
633
- r += 180
634
- }
635
- const uvcs = Math.cos(r * Math.PI / 180)
636
- const uvsn = -Math.sin(r * Math.PI / 180)
637
-
638
- // Get tint (use cached model data and world if available)
639
- const tint = getTint(matchingEFace, cachedModel!.blockName, cachedModel!.blockProps, biome, world)
640
-
641
- const minx = element.from[0]
642
- const miny = element.from[1]
643
- const minz = element.from[2]
644
- const maxx = element.to[0]
645
- const maxy = element.to[1]
646
- const maxz = element.to[2]
647
-
648
- // Calculate transformed direction
649
- const transformedDir = matmul3(globalMatrix, dir)
650
-
651
- // Add 4 vertices for this face
652
- const baseIndex = currentIndex
653
- for (let cornerIdx = 0; cornerIdx < 4; cornerIdx++) {
654
- const pos = corners[cornerIdx]
655
-
656
- // Calculate vertex position (matching reference)
657
- let vertex = [
658
- (pos[0] ? maxx : minx),
659
- (pos[1] ? maxy : miny),
660
- (pos[2] ? maxz : minz)
661
- ]
662
-
663
- // Apply element rotation
664
- vertex = vecadd3(matmul3(localMatrix, vertex), localShift)
665
- // Apply model rotation
666
- vertex = vecadd3(matmul3(globalMatrix, vertex), globalShift)
667
- // Convert to block coordinates (0-1)
668
- vertex = vertex.map(v => v / 16)
669
-
670
- // World position (relative to section)
671
- const worldPos = [
672
- vertex[0] + (bx & 15) - 8,
673
- vertex[1] + (by & 15) - 8,
674
- vertex[2] + (bz & 15) - 8
675
- ]
676
-
677
- log(`[WASM] Corner ${cornerIdx}: corner=[${pos.join(',')}], vertex=[${vertex.map(v => v.toFixed(3)).join(',')}], worldPos=[${worldPos.map(v => v.toFixed(3)).join(',')}]`)
678
-
679
- positions.push(...worldPos)
680
-
681
- // Normal (transformed direction)
682
- normals.push(transformedDir[0], transformedDir[1], transformedDir[2])
683
-
684
- // Color (with AO and light from WASM) - matching TS formula exactly
685
- const ao = aoValues[cornerIdx]
686
-
687
- // TS calculation:
688
- // baseLight = world.getLight(neighborPos, ...) / 15 (0-1 range)
689
- // cornerLightResult = baseLight * 15 (0-15 range, or interpolated if smooth lighting)
690
- // light = (ao + 1) / 4 * (cornerLightResult / 15)
691
- // finalColor = baseLight * tint * light
692
-
693
- // WASM provides lightValues in 0-1 range (already divided by 15)
694
- // But WASM light calculation seems to return 0.0, so we need to handle that
695
- // In the test case, TypeScript gets baseLight = 1.0 (full brightness)
696
- // So we should use 1.0 as the base light value when WASM returns 0
697
- const cornerLight15 = (lightValues[cornerIdx] ?? 1) * 15
698
- const faceDir = transformedDir as [number, number, number]
699
- const light = computeMesherVertexLight(world, ao, cornerLight15, faceDir)
700
-
701
- colors.push(tint[0] * light, tint[1] * light, tint[2] * light)
702
-
703
- // UV calculation (matching reference exactly)
704
- const baseu = (pos[3] - 0.5) * uvcs - (pos[4] - 0.5) * uvsn + 0.5
705
- const basev = (pos[3] - 0.5) * uvsn + (pos[4] - 0.5) * uvcs + 0.5
706
- const finalU = baseu * su + u
707
- const finalV = basev * sv + v
708
- 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}`)
709
- uvs.push(finalU, finalV)
710
-
711
- currentIndex++
712
- }
713
-
714
- // Add indices (2 triangles) - matching TS AO-optimized winding
715
- // TS uses: if (doAO && aos[0] + aos[3] >= aos[1] + aos[2]) { optimized } else { standard }
716
- let tri1: number[], tri2: number[]
717
- if (aoValues[0] + aoValues[3] >= aoValues[1] + aoValues[2]) {
718
- // AO-optimized winding
719
- tri1 = [baseIndex, baseIndex + 3, baseIndex + 2]
720
- tri2 = [baseIndex, baseIndex + 1, baseIndex + 3]
721
- log(`[WASM] Indices (AO optimized): tri1=[${tri1.join(',')}], tri2=[${tri2.join(',')}], aos=[${aoValues.join(',')}]`)
722
- } else {
723
- // Standard winding
724
- tri1 = [baseIndex, baseIndex + 1, baseIndex + 2]
725
- tri2 = [baseIndex + 2, baseIndex + 1, baseIndex + 3]
726
- log(`[WASM] Indices (standard): tri1=[${tri1.join(',')}], tri2=[${tri2.join(',')}], aos=[${aoValues.join(',')}]`)
727
- }
728
- indices.push(...tri1, ...tri2)
729
- }
575
+ if (shaderResources) {
576
+ const modelVars = cachedModel.models[0]
577
+ const model = modelVars?.[0]
578
+ const element = model?.elements?.[0]
579
+ if (model && element) {
580
+ const doAO = (model as { ao?: boolean }).ao ?? cachedModel.boundingBox !== 'empty'
581
+ const emitted = tryBuildShaderCubeInstances(
582
+ block,
583
+ {
584
+ blockName: cachedModel.blockName,
585
+ blockProps: cachedModel.blockProps,
586
+ isCube: cachedModel.isCube,
587
+ model,
588
+ },
589
+ model,
590
+ {
591
+ sectionOrigin: { x: sectionOx, y: sectionOy, z: sectionOz },
592
+ sectionHeight,
593
+ biome,
594
+ tintPalette: shaderResources.tintPalette,
595
+ textureIndexMapping: shaderResources.textureIndexMapping,
596
+ doAO,
597
+ },
598
+ shaderWordBuffer,
599
+ )
600
+ if (emitted) continue
730
601
  }
731
602
  }
732
603
 
@@ -1002,7 +873,9 @@ export function renderWasmOutputToGeometry(
1002
873
  }
1003
874
  }
1004
875
 
1005
- const result = {
876
+ const shaderCubes = buildShaderCubesFromWords(shaderWordBuffer)
877
+
878
+ const result: ExportedSection = {
1006
879
  key: sectionKey,
1007
880
  position: sectionPosition,
1008
881
  geometry: {
@@ -1012,6 +885,7 @@ export function renderWasmOutputToGeometry(
1012
885
  uvs,
1013
886
  indices,
1014
887
  },
888
+ ...(shaderCubes ? { shaderCubes } : {}),
1015
889
  }
1016
890
 
1017
891
  log(`[WASM] Final geometry summary:`)
@@ -1059,10 +933,10 @@ export function renderWasmOutputToGeometry(
1059
933
  export function splitColumnWasmOutputToSections(
1060
934
  fullColumnOutput: WasmGeometryOutput,
1061
935
  requestedSectionKeys: Array<{ x: number, y: number, z: number }>,
1062
- ctx: { version: string, world?: World, sectionHeight?: number }
936
+ ctx: { version: string, world?: World, sectionHeight?: number, shaderCubes?: boolean }
1063
937
  ): Map<string, { exported: ExportedSection, blocksCount: number }> {
1064
938
  const { version, world } = ctx
1065
- const sectionHeight = ctx.sectionHeight ?? 16
939
+ const sectionHeight = ctx.sectionHeight ?? SECTION_HEIGHT
1066
940
 
1067
941
  // Bucket blocks by section Y once, so we don't re-scan the full column
1068
942
  // for every requested section. Bucket key = section-relative chunk Y
@@ -1101,7 +975,8 @@ export function splitColumnWasmOutputToSections(
1101
975
  version,
1102
976
  sectionKey,
1103
977
  sectionPosition,
1104
- world
978
+ world,
979
+ { sectionHeight, shaderCubes: ctx.shaderCubes },
1105
980
  )
1106
981
  out.set(sectionKey, { exported, blocksCount: sectionBlocks.length })
1107
982
  }
@@ -1121,7 +996,9 @@ export function wasmOutputToExportFormat(
1121
996
  cameraRotation = { pitch: 0, yaw: 0 },
1122
997
  world?: World
1123
998
  ): ExportedWorldGeometry {
1124
- const section = renderWasmOutputToGeometry(wasmOutput, version, sectionKey, sectionPosition, world)
999
+ const section = renderWasmOutputToGeometry(wasmOutput, version, sectionKey, sectionPosition, world, {
1000
+ shaderCubes: true,
1001
+ })
1125
1002
 
1126
1003
  return {
1127
1004
  version,