minecraft-renderer 0.1.84 → 0.1.86
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.map +2 -2
- package/dist/mesherWasm.js +214 -214
- package/dist/minecraft-renderer.js +15 -19
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +4 -8
- package/package.json +1 -1
- package/src/lib/worldrendererCommon.ts +7 -0
- package/src/mesher-shared/shared.ts +5 -0
- package/src/three/modules/starfield.ts +3 -16
- package/src/wasm-mesher/bridge/render-from-wasm.ts +51 -21
- package/src/wasm-mesher/tests/blendEmissionOrder.test.ts +123 -0
- package/src/wasm-mesher/tests/crossColumnCulling.test.ts +205 -0
- package/src/wasm-mesher/tests/mesherWasmChunkBuffer.test.ts +24 -0
- package/src/wasm-mesher/tests/mesherWasmNeighborhood.test.ts +91 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +148 -69
- package/src/wasm-mesher/worker/mesherWasmChunkBuffer.ts +28 -0
- package/src/wasm-mesher/worker/mesherWasmNeighborhood.ts +125 -0
package/package.json
CHANGED
|
@@ -676,6 +676,13 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
|
|
|
676
676
|
const heightmap = new Int16Array(data.heightmap)
|
|
677
677
|
this.reactiveState.world.heightmaps[data.key] = heightmap
|
|
678
678
|
}
|
|
679
|
+
|
|
680
|
+
if (data.type === 'neighborDataArrived') {
|
|
681
|
+
const sectionHeight = this.getSectionHeight()
|
|
682
|
+
for (let y = this.worldMinYRender; y < this.worldSizeParams.worldHeight; y += sectionHeight) {
|
|
683
|
+
this.setSectionDirty(new Vec3(data.x, y, data.z))
|
|
684
|
+
}
|
|
685
|
+
}
|
|
679
686
|
}
|
|
680
687
|
|
|
681
688
|
downloadMesherLog() {
|
|
@@ -105,7 +105,12 @@ export interface MesherMainEvents {
|
|
|
105
105
|
// Per-event counts for the column-mode conversion cache.
|
|
106
106
|
preCacheHits?: number
|
|
107
107
|
preCacheMisses?: number
|
|
108
|
+
chunkCount?: number
|
|
109
|
+
worldColumns3x3?: number
|
|
110
|
+
parsedCache3x3?: number
|
|
111
|
+
columnMeshPath?: string
|
|
108
112
|
}
|
|
113
|
+
neighborDataArrived: { type: 'neighborDataArrived'; x: number; z: number; workerIndex: number }
|
|
109
114
|
blockStateModelInfo: { type: 'blockStateModelInfo'; info: Record<string, BlockStateModelInfo> }
|
|
110
115
|
heightmap: { type: 'heightmap'; key: string; heightmap: Int16Array }
|
|
111
116
|
/** Reply to `{ type: 'mc-web-ping', t?, workerIndex? }` from the main thread (not batched in worker). */
|
|
@@ -9,7 +9,7 @@ const threeVersion = parseInt(THREE.REVISION.replaceAll(/\D+/g, ''), 10)
|
|
|
9
9
|
class StarfieldMaterial extends THREE.ShaderMaterial {
|
|
10
10
|
constructor() {
|
|
11
11
|
super({
|
|
12
|
-
uniforms: { time: { value: 0 }
|
|
12
|
+
uniforms: { time: { value: 0 } },
|
|
13
13
|
vertexShader: /* glsl */ `
|
|
14
14
|
uniform float time;
|
|
15
15
|
attribute float size;
|
|
@@ -23,13 +23,9 @@ class StarfieldMaterial extends THREE.ShaderMaterial {
|
|
|
23
23
|
}`,
|
|
24
24
|
fragmentShader: /* glsl */ `
|
|
25
25
|
uniform sampler2D pointTexture;
|
|
26
|
-
uniform float fade;
|
|
27
26
|
varying vec3 vColor;
|
|
28
27
|
void main() {
|
|
29
|
-
|
|
30
|
-
// the only way to dim stars — scene fog never reaches this shader. Driven by the
|
|
31
|
-
// rain state so stars disappear in rain like in vanilla.
|
|
32
|
-
gl_FragColor = vec4(vColor * fade, 1.0);
|
|
28
|
+
gl_FragColor = vec4(vColor, 1.0);
|
|
33
29
|
|
|
34
30
|
#include <tonemapping_fragment>
|
|
35
31
|
#include <${threeVersion >= 154 ? 'colorspace_fragment' : 'encodings_fragment'}>
|
|
@@ -43,8 +39,6 @@ export class StarfieldModule implements RendererModuleController {
|
|
|
43
39
|
private timer = new THREE.Timer()
|
|
44
40
|
private enabled = false
|
|
45
41
|
private currentTime?: number
|
|
46
|
-
/** Current star brightness multiplier; lerps toward 0 while raining, 1 otherwise. */
|
|
47
|
-
private fade = 1
|
|
48
42
|
|
|
49
43
|
constructor(private readonly worldRenderer: WorldRendererThree) {}
|
|
50
44
|
|
|
@@ -76,20 +70,13 @@ export class StarfieldModule implements RendererModuleController {
|
|
|
76
70
|
return this.currentTime > nightTime && this.currentTime < morningStart
|
|
77
71
|
}
|
|
78
72
|
|
|
79
|
-
render?: (deltaTime: number) => void =
|
|
73
|
+
render?: (deltaTime: number) => void = () => {
|
|
80
74
|
if (!this.points) return
|
|
81
75
|
this.points.position.set(0, 0, 0)
|
|
82
76
|
|
|
83
77
|
const material = this.points.material as StarfieldMaterial
|
|
84
78
|
this.timer.update(performance.now())
|
|
85
79
|
material.uniforms.time.value = this.timer.getElapsed() * 0.2
|
|
86
|
-
|
|
87
|
-
// Fade stars out while raining (vanilla scales star brightness by 1 - rainLevel).
|
|
88
|
-
// isRaining is a boolean here, so ease toward the target instead of snapping.
|
|
89
|
-
const target = this.worldRenderer.worldRendererConfig.isRaining ? 0 : 1
|
|
90
|
-
const t = Math.min(1, deltaTime * 2)
|
|
91
|
-
this.fade += (target - this.fade) * t
|
|
92
|
-
material.uniforms.fade.value = this.fade
|
|
93
80
|
}
|
|
94
81
|
|
|
95
82
|
/**
|
|
@@ -559,13 +559,21 @@ export function renderWasmOutputToGeometry(
|
|
|
559
559
|
const uvs: number[] = []
|
|
560
560
|
const indices: number[] = []
|
|
561
561
|
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
const
|
|
566
|
-
const
|
|
567
|
-
const
|
|
568
|
-
const
|
|
562
|
+
const blockBlendPositions: number[] = []
|
|
563
|
+
const blockBlendNormals: number[] = []
|
|
564
|
+
const blockBlendColors: number[] = []
|
|
565
|
+
const blockBlendSkyLights: number[] = []
|
|
566
|
+
const blockBlendBlockLights: number[] = []
|
|
567
|
+
const blockBlendUvs: number[] = []
|
|
568
|
+
const blockBlendIndices: number[] = []
|
|
569
|
+
|
|
570
|
+
const liquidPositions: number[] = []
|
|
571
|
+
const liquidNormals: number[] = []
|
|
572
|
+
const liquidColors: number[] = []
|
|
573
|
+
const liquidSkyLights: number[] = []
|
|
574
|
+
const liquidBlockLights: number[] = []
|
|
575
|
+
const liquidUvs: number[] = []
|
|
576
|
+
const liquidIndices: number[] = []
|
|
569
577
|
|
|
570
578
|
const liquidQueue: Array<{
|
|
571
579
|
pos: Vec3
|
|
@@ -691,13 +699,13 @@ export function renderWasmOutputToGeometry(
|
|
|
691
699
|
if (!models || models.length == 0) continue
|
|
692
700
|
|
|
693
701
|
const routeToBlend = prismBlock.transparent && isSemiTransparentBlockName(cachedModel.blockName)
|
|
694
|
-
const tgtPos = routeToBlend ?
|
|
695
|
-
const tgtNorm = routeToBlend ?
|
|
696
|
-
const tgtCol = routeToBlend ?
|
|
697
|
-
const tgtSky = routeToBlend ?
|
|
698
|
-
const tgtBlock = routeToBlend ?
|
|
699
|
-
const tgtUv = routeToBlend ?
|
|
700
|
-
const tgtIdx = routeToBlend ?
|
|
702
|
+
const tgtPos = routeToBlend ? blockBlendPositions : positions
|
|
703
|
+
const tgtNorm = routeToBlend ? blockBlendNormals : normals
|
|
704
|
+
const tgtCol = routeToBlend ? blockBlendColors : colors
|
|
705
|
+
const tgtSky = routeToBlend ? blockBlendSkyLights : skyLights
|
|
706
|
+
const tgtBlock = routeToBlend ? blockBlendBlockLights : blockLights
|
|
707
|
+
const tgtUv = routeToBlend ? blockBlendUvs : uvs
|
|
708
|
+
const tgtIdx = routeToBlend ? blockBlendIndices : indices
|
|
701
709
|
|
|
702
710
|
const faceNameToIndex: Record<string, number> = {
|
|
703
711
|
up: 0,
|
|
@@ -947,17 +955,39 @@ export function renderWasmOutputToGeometry(
|
|
|
947
955
|
q.biome,
|
|
948
956
|
q.water,
|
|
949
957
|
q.isRealWater,
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
958
|
+
liquidPositions,
|
|
959
|
+
liquidNormals,
|
|
960
|
+
liquidColors,
|
|
961
|
+
liquidSkyLights,
|
|
962
|
+
liquidBlockLights,
|
|
963
|
+
liquidUvs,
|
|
964
|
+
liquidIndices
|
|
957
965
|
)
|
|
958
966
|
}
|
|
959
967
|
}
|
|
960
968
|
|
|
969
|
+
let blendPositions: number[] = []
|
|
970
|
+
let blendNormals: number[] = []
|
|
971
|
+
let blendColors: number[] = []
|
|
972
|
+
let blendSkyLights: number[] = []
|
|
973
|
+
let blendBlockLights: number[] = []
|
|
974
|
+
let blendUvs: number[] = []
|
|
975
|
+
const blendIndices: number[] = []
|
|
976
|
+
|
|
977
|
+
if (liquidPositions.length > 0 || blockBlendPositions.length > 0) {
|
|
978
|
+
const liquidVertexCount = liquidPositions.length / 3
|
|
979
|
+
blendPositions = liquidPositions.concat(blockBlendPositions)
|
|
980
|
+
blendNormals = liquidNormals.concat(blockBlendNormals)
|
|
981
|
+
blendColors = liquidColors.concat(blockBlendColors)
|
|
982
|
+
blendSkyLights = liquidSkyLights.concat(blockBlendSkyLights)
|
|
983
|
+
blendBlockLights = liquidBlockLights.concat(blockBlendBlockLights)
|
|
984
|
+
blendUvs = liquidUvs.concat(blockBlendUvs)
|
|
985
|
+
blendIndices.push(...liquidIndices)
|
|
986
|
+
for (const idx of blockBlendIndices) {
|
|
987
|
+
blendIndices.push(idx + liquidVertexCount)
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
961
991
|
const shaderCubes = buildShaderCubesFromWords(shaderWordBuffer)
|
|
962
992
|
|
|
963
993
|
const result: ExportedSection = {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { beforeAll, beforeEach, expect, test } from 'vitest'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import Chunks from 'prismarine-chunk'
|
|
7
|
+
import MinecraftData from 'minecraft-data'
|
|
8
|
+
import { Vec3 } from 'vec3'
|
|
9
|
+
import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
|
|
10
|
+
import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
|
|
11
|
+
import { World } from '../../mesher-shared/world'
|
|
12
|
+
import { setBlockStatesData } from '../../mesher-shared/models'
|
|
13
|
+
import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
|
|
14
|
+
import { convertChunkToWasm, getBlockMeta } from '../bridge/convertChunk'
|
|
15
|
+
import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
|
|
16
|
+
|
|
17
|
+
const VERSION = '1.17.1'
|
|
18
|
+
const SECTION_Y = 0
|
|
19
|
+
const ICE_Y = 2
|
|
20
|
+
const WATER_Y = 1
|
|
21
|
+
const WORLD_MIN_Y = 0
|
|
22
|
+
const WORLD_MAX_Y = 256
|
|
23
|
+
const COLUMN_HEIGHT = WORLD_MAX_Y - WORLD_MIN_Y
|
|
24
|
+
|
|
25
|
+
let wasmModule: typeof import('../runtime-build/wasm_mesher.js')
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
wasmModule = await import('../runtime-build/wasm_mesher.js')
|
|
29
|
+
const wasmDir = dirname(fileURLToPath(import.meta.url))
|
|
30
|
+
const wasmBytes = readFileSync(join(wasmDir, '../runtime-build/wasm_mesher_bg.wasm'))
|
|
31
|
+
wasmModule.initSync(wasmBytes)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
resetFaceOcclusionCache()
|
|
36
|
+
const mcData = MinecraftData(VERSION)
|
|
37
|
+
setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
|
|
38
|
+
;(globalThis as any).__wasmBlockModelCache = new Map()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
function resolveStateId(name: string) {
|
|
42
|
+
const mcData = MinecraftData(VERSION)
|
|
43
|
+
const block = mcData.blocksByName[name]
|
|
44
|
+
if (!block) throw new Error(`Unknown block: ${name}`)
|
|
45
|
+
return block.defaultState
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildIceOverWaterWorld(): World {
|
|
49
|
+
const Chunk = Chunks(VERSION) as any
|
|
50
|
+
const chunk = new Chunk(undefined as any)
|
|
51
|
+
const iceId = resolveStateId('ice')
|
|
52
|
+
const waterId = resolveStateId('water')
|
|
53
|
+
|
|
54
|
+
for (let z = 0; z < 16; z++) {
|
|
55
|
+
for (let x = 0; x < 16; x++) {
|
|
56
|
+
chunk.setBlockStateId(new Vec3(x, WATER_Y, z), waterId)
|
|
57
|
+
chunk.setBlockStateId(new Vec3(x, ICE_Y, z), iceId)
|
|
58
|
+
chunk.setBlockLight(new Vec3(x, ICE_Y, z), 15)
|
|
59
|
+
chunk.setSkyLight(new Vec3(x, ICE_Y, z), 15)
|
|
60
|
+
chunk.setBlockLight(new Vec3(x, WATER_Y, z), 15)
|
|
61
|
+
chunk.setSkyLight(new Vec3(x, WATER_Y, z), 15)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const world = new World(VERSION)
|
|
66
|
+
world.addColumn(0, 0, chunk.toJson())
|
|
67
|
+
return world
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findLiquidBeforeBlockIndexSplit(indices: number[]) {
|
|
71
|
+
for (let split = 1; split < indices.length; split++) {
|
|
72
|
+
const prefix = indices.slice(0, split)
|
|
73
|
+
const suffix = indices.slice(split)
|
|
74
|
+
const maxPrefix = Math.max(...prefix)
|
|
75
|
+
const minSuffix = Math.min(...suffix)
|
|
76
|
+
if (maxPrefix < minSuffix) {
|
|
77
|
+
return { split, liquidVertexCount: minSuffix }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test('blend emission order: liquid quads precede ice block quads in the index buffer', () => {
|
|
84
|
+
const world = buildIceOverWaterWorld()
|
|
85
|
+
const meta = getBlockMeta(VERSION)
|
|
86
|
+
const column = world.getColumn(0, 0)!
|
|
87
|
+
const conversion = convertChunkToWasm(column, VERSION, 0, 0, WORLD_MIN_Y, WORLD_MAX_Y)
|
|
88
|
+
const wasmResult = wasmModule.generate_geometry(
|
|
89
|
+
0,
|
|
90
|
+
WORLD_MIN_Y,
|
|
91
|
+
0,
|
|
92
|
+
COLUMN_HEIGHT,
|
|
93
|
+
WORLD_MIN_Y,
|
|
94
|
+
WORLD_MAX_Y,
|
|
95
|
+
WORLD_MIN_Y,
|
|
96
|
+
conversion.blockStates,
|
|
97
|
+
conversion.blockLight,
|
|
98
|
+
conversion.skyLight,
|
|
99
|
+
conversion.biomesArray,
|
|
100
|
+
meta.invisibleBlocks,
|
|
101
|
+
meta.transparentBlocks,
|
|
102
|
+
meta.noAoBlocks,
|
|
103
|
+
meta.cullIdenticalBlocks,
|
|
104
|
+
meta.occludingBlocks,
|
|
105
|
+
true,
|
|
106
|
+
false,
|
|
107
|
+
15
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const section = renderWasmOutputToGeometry(wasmResult, VERSION, `0,${SECTION_Y},0`, { x: 8, y: 8, z: 8 }, world, {
|
|
111
|
+
sectionHeight: 16,
|
|
112
|
+
shaderCubes: false
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const blend = section.blendGeometry
|
|
116
|
+
expect(blend).toBeDefined()
|
|
117
|
+
expect(blend!.indices.length).toBeGreaterThan(0)
|
|
118
|
+
|
|
119
|
+
const split = findLiquidBeforeBlockIndexSplit(blend!.indices)
|
|
120
|
+
expect(split).not.toBeNull()
|
|
121
|
+
expect(split!.split).toBeGreaterThan(0)
|
|
122
|
+
expect(split!.liquidVertexCount).toBeGreaterThan(0)
|
|
123
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { beforeAll, beforeEach, describe, expect, test } from 'vitest'
|
|
3
|
+
import { readFileSync } from 'node:fs'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import Chunks from 'prismarine-chunk'
|
|
7
|
+
import MinecraftData from 'minecraft-data'
|
|
8
|
+
import { Vec3 } from 'vec3'
|
|
9
|
+
import PrismarineBlockLoader from 'prismarine-block'
|
|
10
|
+
import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
|
|
11
|
+
import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
|
|
12
|
+
import { setBlockStatesData } from '../../mesher-shared/models'
|
|
13
|
+
import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
|
|
14
|
+
import { convertChunkToWasm, getBlockMeta } from '../bridge/convertChunk'
|
|
15
|
+
import type { WasmGeometryOutput } from '../bridge/render-from-wasm'
|
|
16
|
+
|
|
17
|
+
const VERSION = '1.17.1'
|
|
18
|
+
const ICE_Y = 62
|
|
19
|
+
const WATER_Y = 61
|
|
20
|
+
const WORLD_MIN_Y = 0
|
|
21
|
+
// Full overworld height (256) makes multi-column WASM meshing exceed CI's 5s
|
|
22
|
+
// default timeout. Culling only needs the section band that contains ice/water.
|
|
23
|
+
const WORLD_MAX_Y = 80
|
|
24
|
+
const COLUMN_HEIGHT = WORLD_MAX_Y - WORLD_MIN_Y
|
|
25
|
+
|
|
26
|
+
const EAST_FACE = 1 << 2
|
|
27
|
+
const WEST_FACE = 1 << 3
|
|
28
|
+
|
|
29
|
+
let wasmModule: typeof import('../runtime-build/wasm_mesher.js')
|
|
30
|
+
|
|
31
|
+
beforeAll(async () => {
|
|
32
|
+
wasmModule = await import('../runtime-build/wasm_mesher.js')
|
|
33
|
+
const wasmDir = dirname(fileURLToPath(import.meta.url))
|
|
34
|
+
const wasmBytes = readFileSync(join(wasmDir, '../runtime-build/wasm_mesher_bg.wasm'))
|
|
35
|
+
wasmModule.initSync(wasmBytes)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
resetFaceOcclusionCache()
|
|
40
|
+
const mcData = MinecraftData(VERSION)
|
|
41
|
+
setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
|
|
42
|
+
;(globalThis as any).__wasmBlockModelCache = new Map()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function resolveStateId(name: string) {
|
|
46
|
+
const mcData = MinecraftData(VERSION)
|
|
47
|
+
const block = mcData.blocksByName[name]
|
|
48
|
+
if (!block) throw new Error(`Unknown block: ${name}`)
|
|
49
|
+
return block.defaultState
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildIceOverWaterColumn() {
|
|
53
|
+
const Chunk = Chunks(VERSION) as any
|
|
54
|
+
const chunk = new Chunk(undefined as any)
|
|
55
|
+
const iceId = resolveStateId('ice')
|
|
56
|
+
const waterId = resolveStateId('water')
|
|
57
|
+
|
|
58
|
+
for (let z = 0; z < 16; z++) {
|
|
59
|
+
for (let x = 0; x < 16; x++) {
|
|
60
|
+
chunk.setBlockStateId(new Vec3(x, WATER_Y, z), waterId)
|
|
61
|
+
chunk.setBlockStateId(new Vec3(x, ICE_Y, z), iceId)
|
|
62
|
+
chunk.setBlockLight(new Vec3(x, ICE_Y, z), 15)
|
|
63
|
+
chunk.setSkyLight(new Vec3(x, ICE_Y, z), 15)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return chunk.toJson()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function meshColumns(columnOrigins: Array<{ x: number; z: number }>, targetX: number, targetZ: number, options?: { forceMulti?: boolean }): WasmGeometryOutput {
|
|
70
|
+
const meta = getBlockMeta(VERSION)
|
|
71
|
+
const conversions = columnOrigins.map(({ x, z }) => {
|
|
72
|
+
const chunkJson = buildIceOverWaterColumn()
|
|
73
|
+
return convertChunkToWasm(chunkJson, VERSION, x, z, WORLD_MIN_Y, WORLD_MAX_Y)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (conversions.length === 1 && !options?.forceMulti) {
|
|
77
|
+
const c = conversions[0]
|
|
78
|
+
return wasmModule.generate_geometry(
|
|
79
|
+
targetX,
|
|
80
|
+
WORLD_MIN_Y,
|
|
81
|
+
targetZ,
|
|
82
|
+
COLUMN_HEIGHT,
|
|
83
|
+
WORLD_MIN_Y,
|
|
84
|
+
WORLD_MAX_Y,
|
|
85
|
+
WORLD_MIN_Y,
|
|
86
|
+
c.blockStates,
|
|
87
|
+
c.blockLight,
|
|
88
|
+
c.skyLight,
|
|
89
|
+
c.biomesArray,
|
|
90
|
+
meta.invisibleBlocks,
|
|
91
|
+
meta.transparentBlocks,
|
|
92
|
+
meta.noAoBlocks,
|
|
93
|
+
meta.cullIdenticalBlocks,
|
|
94
|
+
meta.occludingBlocks,
|
|
95
|
+
true,
|
|
96
|
+
false,
|
|
97
|
+
15
|
|
98
|
+
) as WasmGeometryOutput
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const perChunkLen = conversions[0].blockStates.length
|
|
102
|
+
const xs = new Int32Array(columnOrigins.length)
|
|
103
|
+
const zs = new Int32Array(columnOrigins.length)
|
|
104
|
+
const blockStatesAll = new Uint16Array(perChunkLen * columnOrigins.length)
|
|
105
|
+
const blockLightAll = new Uint8Array(perChunkLen * columnOrigins.length)
|
|
106
|
+
const skyLightAll = new Uint8Array(perChunkLen * columnOrigins.length)
|
|
107
|
+
const biomesAll = new Uint8Array(perChunkLen * columnOrigins.length)
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < columnOrigins.length; i++) {
|
|
110
|
+
const c = conversions[i]
|
|
111
|
+
xs[i] = columnOrigins[i].x
|
|
112
|
+
zs[i] = columnOrigins[i].z
|
|
113
|
+
blockStatesAll.set(c.blockStates, perChunkLen * i)
|
|
114
|
+
blockLightAll.set(c.blockLight, perChunkLen * i)
|
|
115
|
+
skyLightAll.set(c.skyLight, perChunkLen * i)
|
|
116
|
+
biomesAll.set(c.biomesArray, perChunkLen * i)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const first = conversions[0]
|
|
120
|
+
return (wasmModule as any).generate_geometry_multi(
|
|
121
|
+
targetX,
|
|
122
|
+
WORLD_MIN_Y,
|
|
123
|
+
targetZ,
|
|
124
|
+
COLUMN_HEIGHT,
|
|
125
|
+
WORLD_MIN_Y,
|
|
126
|
+
WORLD_MAX_Y,
|
|
127
|
+
WORLD_MIN_Y,
|
|
128
|
+
xs,
|
|
129
|
+
zs,
|
|
130
|
+
blockStatesAll,
|
|
131
|
+
blockLightAll,
|
|
132
|
+
skyLightAll,
|
|
133
|
+
biomesAll,
|
|
134
|
+
first.invisibleBlocks,
|
|
135
|
+
first.transparentBlocks,
|
|
136
|
+
first.noAoBlocks,
|
|
137
|
+
first.cullIdenticalBlocks,
|
|
138
|
+
first.occludingBlocks,
|
|
139
|
+
true,
|
|
140
|
+
false,
|
|
141
|
+
15
|
|
142
|
+
) as WasmGeometryOutput
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function countIceBorderFaces(output: WasmGeometryOutput, iceStateId: number, planeX: number, faceBit: number) {
|
|
146
|
+
let count = 0
|
|
147
|
+
for (const block of output.blocks) {
|
|
148
|
+
if (block.block_state_id !== iceStateId) continue
|
|
149
|
+
const [bx, by] = block.position
|
|
150
|
+
if (bx !== planeX || by !== ICE_Y) continue
|
|
151
|
+
if ((block.visible_faces & faceBit) !== 0) count++
|
|
152
|
+
}
|
|
153
|
+
return count
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe('cross-column ice culling', { timeout: 15_000 }, () => {
|
|
157
|
+
test('two adjacent columns produce zero border faces', () => {
|
|
158
|
+
const iceStateId = resolveStateId('ice')
|
|
159
|
+
const output = meshColumns(
|
|
160
|
+
[
|
|
161
|
+
{ x: 0, z: 0 },
|
|
162
|
+
{ x: 16, z: 0 }
|
|
163
|
+
],
|
|
164
|
+
0,
|
|
165
|
+
0
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
expect(countIceBorderFaces(output, iceStateId, 15, EAST_FACE)).toBe(0)
|
|
169
|
+
expect(countIceBorderFaces(output, iceStateId, 16, WEST_FACE)).toBe(0)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
test('negative coordinates', () => {
|
|
173
|
+
const iceStateId = resolveStateId('ice')
|
|
174
|
+
const ax = -504928
|
|
175
|
+
const bx = ax + 16
|
|
176
|
+
const z = 496768
|
|
177
|
+
const output = meshColumns(
|
|
178
|
+
[
|
|
179
|
+
{ x: ax, z },
|
|
180
|
+
{ x: bx, z }
|
|
181
|
+
],
|
|
182
|
+
ax,
|
|
183
|
+
z
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(countIceBorderFaces(output, iceStateId, ax + 15, EAST_FACE)).toBe(0)
|
|
187
|
+
expect(countIceBorderFaces(output, iceStateId, bx, WEST_FACE)).toBe(0)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('healed mesh has fewer blocks at the shared boundary plane', () => {
|
|
191
|
+
const alone = meshColumns([{ x: 0, z: 0 }], 0, 0, { forceMulti: true })
|
|
192
|
+
const pair = meshColumns(
|
|
193
|
+
[
|
|
194
|
+
{ x: 0, z: 0 },
|
|
195
|
+
{ x: 16, z: 0 }
|
|
196
|
+
],
|
|
197
|
+
0,
|
|
198
|
+
0
|
|
199
|
+
)
|
|
200
|
+
const aloneBoundaryBlocks = alone.blocks.filter(b => b.position[0] === 15 && b.position[1] === ICE_Y).length
|
|
201
|
+
const pairBoundaryBlocks = pair.blocks.filter(b => b.position[0] === 15 && b.position[1] === ICE_Y).length
|
|
202
|
+
expect(aloneBoundaryBlocks).toBeGreaterThanOrEqual(pairBoundaryBlocks)
|
|
203
|
+
expect(pair.blocks.filter(b => b.position[0] === 16 && b.position[1] === ICE_Y && (b.visible_faces & WEST_FACE) !== 0).length).toBe(0)
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import { PendingChunkBuffer } from '../worker/mesherWasmChunkBuffer'
|
|
4
|
+
|
|
5
|
+
describe('PendingChunkBuffer', () => {
|
|
6
|
+
test('buffers chunk messages until drained', () => {
|
|
7
|
+
const buffer = new PendingChunkBuffer()
|
|
8
|
+
buffer.enqueue({ x: 0, z: 0, chunk: { sections: [] } })
|
|
9
|
+
buffer.enqueue({ x: 16, z: 0, chunk: { sections: [] } })
|
|
10
|
+
expect(buffer.size).toBe(2)
|
|
11
|
+
|
|
12
|
+
const applied: number[] = []
|
|
13
|
+
buffer.drain(msg => applied.push(msg.x))
|
|
14
|
+
expect(applied).toEqual([0, 16])
|
|
15
|
+
expect(buffer.size).toBe(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('clear drops pending messages', () => {
|
|
19
|
+
const buffer = new PendingChunkBuffer()
|
|
20
|
+
buffer.enqueue({ x: 0, z: 0, chunk: {} })
|
|
21
|
+
buffer.clear()
|
|
22
|
+
expect(buffer.size).toBe(0)
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { describe, expect, test } from 'vitest'
|
|
3
|
+
import {
|
|
4
|
+
PendingNeighborHealTracker,
|
|
5
|
+
collectChunksForColumnUnion,
|
|
6
|
+
columnDataAvailable,
|
|
7
|
+
countParsedCache3x3,
|
|
8
|
+
countWorldColumns3x3
|
|
9
|
+
} from '../worker/mesherWasmNeighborhood'
|
|
10
|
+
|
|
11
|
+
describe('collectChunksForColumnUnion', () => {
|
|
12
|
+
test('includes cache-only side neighbors not yet in world.columns', () => {
|
|
13
|
+
const worldCols = new Map<string, any>([['0,0', { id: 'A' }]])
|
|
14
|
+
const packet = new Set(['16,0'])
|
|
15
|
+
|
|
16
|
+
const result = collectChunksForColumnUnion(0, 0, (x, z) => worldCols.get(`${x},${z}`) ?? null, { raw: packet, v17: new Set(), v16: new Set() })
|
|
17
|
+
|
|
18
|
+
expect(result).toHaveLength(2)
|
|
19
|
+
expect(result[0]).toEqual({ x: 0, z: 0, chunk: { id: 'A' } })
|
|
20
|
+
expect(result[1]).toEqual({ x: 16, z: 0, chunk: null })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('returns full 3x3 when both pipelines are complete', () => {
|
|
24
|
+
const worldCols = new Map<string, any>()
|
|
25
|
+
const packet = new Set<string>()
|
|
26
|
+
for (const dx of [-16, 0, 16]) {
|
|
27
|
+
for (const dz of [-16, 0, 16]) {
|
|
28
|
+
worldCols.set(`${dx},${dz}`, {})
|
|
29
|
+
packet.add(`${dx},${dz}`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const result = collectChunksForColumnUnion(0, 0, (x, z) => worldCols.get(`${x},${z}`) ?? null, {
|
|
34
|
+
raw: packet,
|
|
35
|
+
v17: new Set(),
|
|
36
|
+
v16: new Set()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
expect(result).toHaveLength(9)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('column availability counters', () => {
|
|
44
|
+
test('detects pipeline A lag behind packet caches', () => {
|
|
45
|
+
const worldCols = new Map<string, any>([
|
|
46
|
+
['0,0', {}],
|
|
47
|
+
['16,0', {}],
|
|
48
|
+
['-16,0', {}],
|
|
49
|
+
['0,16', {}]
|
|
50
|
+
])
|
|
51
|
+
const packet = new Set(['0,0', '16,0', '-16,0', '0,16', '0,-16', '16,16', '-16,16', '-16,-16', '16,-16'])
|
|
52
|
+
const caches = { raw: packet, v17: new Set(), v16: new Set() }
|
|
53
|
+
|
|
54
|
+
expect(countWorldColumns3x3(0, 0, (x, z) => worldCols.get(`${x},${z}`) ?? null)).toBe(4)
|
|
55
|
+
expect(countParsedCache3x3(0, 0, caches)).toBe(9)
|
|
56
|
+
expect(columnDataAvailable(0, -16, (x, z) => worldCols.get(`${x},${z}`) ?? null, caches)).toBe(true)
|
|
57
|
+
expect(columnDataAvailable(16, 16, (x, z) => worldCols.get(`${x},${z}`) ?? null, caches)).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('PendingNeighborHealTracker', () => {
|
|
62
|
+
test('queues and releases columns awaiting a missing neighbor', () => {
|
|
63
|
+
const tracker = new PendingNeighborHealTracker()
|
|
64
|
+
tracker.recordMissingSide(0, 0, 16, 0)
|
|
65
|
+
tracker.recordMissingSide(0, 16, 16, 0)
|
|
66
|
+
|
|
67
|
+
expect(tracker.takeColumnsAwaitingNeighbor(16, 0).sort((a, b) => a.z - b.z)).toEqual([
|
|
68
|
+
{ x: 0, z: 0 },
|
|
69
|
+
{ x: 0, z: 16 }
|
|
70
|
+
])
|
|
71
|
+
expect(tracker.takeColumnsAwaitingNeighbor(16, 0)).toEqual([])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('clears column references on unload', () => {
|
|
75
|
+
const tracker = new PendingNeighborHealTracker()
|
|
76
|
+
tracker.recordMissingSide(0, 0, 16, 0)
|
|
77
|
+
tracker.clearColumn(0, 0)
|
|
78
|
+
expect(tracker.takeColumnsAwaitingNeighbor(16, 0)).toEqual([])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('union grows when packet cache arrives for a missing side neighbor (I1 heal)', () => {
|
|
82
|
+
const worldCols = new Map<string, any>([['0,0', {}]])
|
|
83
|
+
const v17 = new Set<string>()
|
|
84
|
+
const caches = { raw: new Set<string>(), v17, v16: new Set<string>() }
|
|
85
|
+
const getColumn = (x: number, z: number) => worldCols.get(`${x},${z}`) ?? null
|
|
86
|
+
|
|
87
|
+
expect(collectChunksForColumnUnion(0, 0, getColumn, caches)).toHaveLength(1)
|
|
88
|
+
v17.add('16,0')
|
|
89
|
+
expect(collectChunksForColumnUnion(0, 0, getColumn, caches)).toHaveLength(2)
|
|
90
|
+
})
|
|
91
|
+
})
|