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.
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +3740 -183
- package/dist/minecraft-renderer.js +332 -60
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +705 -433
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +4 -0
- package/src/graphicsBackend/playerState.ts +1 -0
- package/src/graphicsBackend/rendererOptionsSync.ts +1 -0
- package/src/lib/worldrendererCommon.ts +13 -0
- package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
- package/src/mesher-shared/shared.ts +8 -0
- package/src/playerState/playerState.ts +2 -0
- package/src/three/chunkMeshManager.ts +312 -39
- package/src/three/globalBlockBuffer.ts +292 -0
- package/src/three/menuBackground/config.ts +1 -1
- package/src/three/menuBackground/defaultOptions.ts +27 -19
- package/src/three/modules/sciFiWorldReveal.ts +162 -68
- package/src/three/modules/starfield.ts +9 -1
- package/src/three/sectionRaycastAabb.ts +167 -0
- package/src/three/shaderCubeMesh.ts +93 -0
- package/src/three/shaders/cubeBlockShader.ts +354 -0
- package/src/three/shaders/textureIndexMapping.ts +122 -0
- package/src/three/shaders/tintPalette.ts +198 -0
- package/src/three/worldGeometryExport.ts +53 -25
- package/src/three/worldRendererThree.ts +93 -26
- package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +396 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
- package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
- package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
- 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
|
|
141
|
+
const group = new THREE.Group()
|
|
142
|
+
group.name = 'chunk'
|
|
125
143
|
|
|
126
|
-
geometry.
|
|
127
|
-
if (
|
|
128
|
-
geometry
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
738
|
-
|
|
739
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
//
|
|
995
|
+
// Legacy / deferred-shader meshes (scene-relative raycast)
|
|
981
996
|
const meshes: THREE.Object3D[] = []
|
|
982
|
-
for (const
|
|
983
|
-
|
|
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
|
-
|
|
1204
|
-
if (
|
|
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 (
|
|
552
|
-
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
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 ??
|
|
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,
|