minecraft-renderer 0.1.48 → 0.1.50

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 +2 -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/three/chunkMeshManager.ts +312 -39
  15. package/src/three/globalBlockBuffer.ts +292 -0
  16. package/src/three/menuBackground/config.ts +1 -1
  17. package/src/three/menuBackground/defaultOptions.ts +52 -19
  18. package/src/three/menuBackground/index.ts +5 -1
  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 +56 -23
  28. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  29. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +399 -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
@@ -522,6 +527,12 @@ export class WorldRendererThree extends WorldRendererCommon {
522
527
  this.onReactiveConfigUpdated('defaultSkybox', (value) => {
523
528
  this.skyboxRenderer.updateDefaultSkybox(value)
524
529
  })
530
+ this.onReactiveConfigUpdated('shaderCubeDebugMode', () => {
531
+ this.chunkMeshManager.syncCubeShaderUniforms()
532
+ })
533
+ this.onReactiveConfigUpdated('futuristicReveal', () => {
534
+ this.updateModulesFromConfig()
535
+ })
525
536
 
526
537
  let currentHandRenderer = this.displayOptions.inWorldRenderingConfig.handRenderer
527
538
  this.onReactiveConfigUpdated('handRenderer', (value) => {
@@ -618,6 +629,7 @@ export class WorldRendererThree extends WorldRendererCommon {
618
629
  texture.needsUpdate = true
619
630
  texture.flipY = false
620
631
  this.material.map = texture
632
+ this.chunkMeshManager.syncCubeShaderUniforms()
621
633
 
622
634
  const itemsTexture = loadThreeJsTextureFromBitmap(resources.itemsAtlasImage!)
623
635
  itemsTexture.needsUpdate = true
@@ -734,12 +746,21 @@ export class WorldRendererThree extends WorldRendererCommon {
734
746
  * Optionally update data that are depedendent on the viewer position
735
747
  */
736
748
  updatePosDataChunk(key: string) {
749
+ const sectionObj = this.sectionObjects[key]
750
+ if (!sectionObj) return
751
+
737
752
  const [x, y, z] = key.split(',').map(x => Math.floor(+x / 16))
738
753
  // sum of distances: x + y + z
739
754
  const chunkDistance = Math.abs(x - this.cameraSectionPos.x) + Math.abs(y - this.cameraSectionPos.y) + Math.abs(z - this.cameraSectionPos.z)
740
- const sectionObj = this.sectionObjects[key]
741
- const section = (sectionObj as any).mesh ?? sectionObj.children.find(child => child.name === 'mesh')!
742
- 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
+ }
743
764
  }
744
765
 
745
766
  override updateViewerPosition(pos: Vec3): void {
@@ -820,7 +841,7 @@ export class WorldRendererThree extends WorldRendererCommon {
820
841
  continue
821
842
  }
822
843
 
823
- if (!update.geometry.positions.length) {
844
+ if (!this.chunkMeshManager.sectionHasRenderableContent(update.geometry)) {
824
845
  this.chunkMeshManager.releaseSection(update.key)
825
846
  continue
826
847
  }
@@ -859,7 +880,7 @@ export class WorldRendererThree extends WorldRendererCommon {
859
880
  return
860
881
  }
861
882
 
862
- if (!data.geometry.positions.length) {
883
+ if (!this.chunkMeshManager.sectionHasRenderableContent(data.geometry)) {
863
884
  this.chunkMeshManager.releaseSection(data.key)
864
885
  return
865
886
  }
@@ -965,25 +986,22 @@ export class WorldRendererThree extends WorldRendererCommon {
965
986
  raycaster.set(scenePos, direction)
966
987
  raycaster.far = distance // Limit raycast distance
967
988
 
968
- // Filter to only nearby chunks for performance
969
- const nearbyChunks = Object.values(this.sectionObjects)
970
- .filter(obj => obj.name === 'chunk' && obj.visible)
971
- .filter(obj => {
972
- // Get the mesh child which has the actual geometry
973
- const mesh = obj.children.find(child => child.name === 'mesh')
974
- if (!mesh) return false
975
-
976
- // Check distance from player position to chunk
977
- const chunkWorldPos = this._tpChunkWorldPos
978
- mesh.getWorldPosition(chunkWorldPos)
979
- const distance = scenePos.distanceTo(chunkWorldPos)
980
- return distance < 80 // Only check chunks within 80 blocks
981
- })
989
+ const maxCenterDistance = 80
990
+ const maxCenterDistSq = maxCenterDistance * maxCenterDistance
991
+ const ox = pos.x
992
+ const oy = pos.y
993
+ const oz = pos.z
982
994
 
983
- // Get all mesh children for raycasting
995
+ // Legacy / deferred-shader meshes (scene-relative raycast)
984
996
  const meshes: THREE.Object3D[] = []
985
- for (const chunk of nearbyChunks) {
986
- 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')
987
1005
  if (mesh) meshes.push(mesh)
988
1006
  }
989
1007
 
@@ -991,10 +1009,20 @@ export class WorldRendererThree extends WorldRendererCommon {
991
1009
 
992
1010
  let finalDistance = distance
993
1011
  if (intersects.length > 0) {
994
- // Use intersection distance minus a small offset to prevent clipping
995
1012
  finalDistance = Math.max(0.5, intersects[0].distance - 0.2)
996
1013
  }
997
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
+
998
1026
  const finalPos = new Vec3(
999
1027
  pos.x + direction.x * finalDistance,
1000
1028
  pos.y + direction.y * finalDistance,
@@ -1252,6 +1280,11 @@ export class WorldRendererThree extends WorldRendererCommon {
1252
1280
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
1253
1281
  const cam = this.cameraGroupVr instanceof THREE.Group ? this.cameraGroupVr.children.find(child => child instanceof THREE.PerspectiveCamera) as THREE.PerspectiveCamera : this.camera
1254
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
+ }
1255
1288
  this.renderer.render(this.scene, cam)
1256
1289
 
1257
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,