minecraft-renderer 0.1.79 → 0.1.81
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/README.md +66 -26
- package/dist/mesher.js +36 -36
- package/dist/mesher.js.map +4 -4
- package/dist/mesherWasm.js +217 -217
- package/dist/minecraft-renderer.js +66 -66
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +450 -450
- package/package.json +5 -2
- package/src/bundler/bundlePrepare.ts +3 -8
- package/src/graphicsBackend/appViewer.ts +10 -7
- package/src/graphicsBackend/config.ts +1 -5
- package/src/graphicsBackend/playerState.ts +35 -34
- package/src/graphicsBackend/preloadWorkers.ts +43 -41
- package/src/graphicsBackend/rendererDefaultOptions.ts +68 -70
- package/src/graphicsBackend/rendererOptionsSync.ts +24 -50
- package/src/graphicsBackend/types.ts +2 -2
- package/src/index.ts +6 -27
- package/src/lib/DebugGui.ts +34 -31
- package/src/lib/animationController.ts +9 -9
- package/src/lib/bakeLegacyLight.ts +1 -6
- package/src/lib/bindAbortableListener.test.ts +32 -12
- package/src/lib/bindAbortableListener.ts +14 -6
- package/src/lib/blockEntityLightRegistry.ts +11 -20
- package/src/lib/blockEntityLighting.test.ts +1 -6
- package/src/lib/blockEntityLighting.ts +5 -5
- package/src/lib/buildSharedConfig.mjs +4 -4
- package/src/lib/buildWorkerMcDataIndexes.test.ts +4 -4
- package/src/lib/buildWorkerMcDataIndexes.ts +11 -19
- package/src/lib/cameraBobbing.ts +1 -1
- package/src/lib/canvas2DOverlay.example.ts +13 -33
- package/src/lib/canvas2DOverlay.quickstart.ts +24 -54
- package/src/lib/canvas2DOverlay.ts +28 -47
- package/src/lib/cleanupDecorator.ts +2 -2
- package/src/lib/createPlayerObject.test.ts +2 -7
- package/src/lib/createPlayerObject.ts +18 -25
- package/src/lib/frameTimingCollector.ts +2 -5
- package/src/lib/guiRenderer.ts +57 -42
- package/src/lib/items.ts +15 -9
- package/src/lib/mesherlogReader.ts +5 -6
- package/src/lib/simpleUtils.ts +5 -5
- package/src/lib/skyLight.ts +3 -3
- package/src/lib/smoothSwitcher.ts +11 -18
- package/src/lib/ui/newStats.ts +10 -12
- package/src/lib/utils/proxy.ts +7 -1
- package/src/lib/utils/skins.ts +4 -4
- package/src/lib/utils.ts +1 -1
- package/src/lib/workerMessageSanitize.ts +5 -12
- package/src/lib/workerProxy.ts +52 -42
- package/src/lib/workerSyncOps.test.ts +58 -41
- package/src/lib/worldrendererCommon.reconfigure.test.ts +24 -21
- package/src/lib/worldrendererCommon.removeColumn.test.ts +11 -13
- package/src/lib/worldrendererCommon.ts +349 -211
- package/src/mesher-legacy/mesher.ts +9 -4
- package/src/mesher-legacy/test/a.ts +1 -1
- package/src/mesher-legacy/test/mesherTester.ts +2 -2
- package/src/mesher-legacy/test/playground.ts +1 -1
- package/src/mesher-legacy/test/run/test-js.ts +1 -1
- package/src/mesher-legacy/test/snapshotUtils.ts +3 -8
- package/src/mesher-legacy/test/test-perf.ts +1 -1
- package/src/mesher-legacy/test/tests.test.ts +2 -2
- package/src/mesher-shared/blockEntityMetadata.test.ts +7 -5
- package/src/mesher-shared/blockEntityMetadata.ts +28 -19
- package/src/mesher-shared/blockPropertiesForMeshing.ts +2 -2
- package/src/mesher-shared/computeHeightmap.ts +2 -4
- package/src/mesher-shared/exportedGeometryTypes.ts +3 -3
- package/src/mesher-shared/faceOcclusion.ts +324 -0
- package/src/mesher-shared/models.ts +80 -131
- package/src/mesher-shared/modelsGeometryCommon.ts +30 -15
- package/src/mesher-shared/shared.ts +46 -46
- package/src/mesher-shared/standaloneRenderer.ts +40 -44
- package/src/mesher-shared/tests/faceOcclusion.test.ts +179 -0
- package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +12 -8
- package/src/mesher-shared/vertexShading.ts +2 -11
- package/src/mesher-shared/world.ts +19 -19
- package/src/mesher-shared/worldLighting.test.ts +2 -2
- package/src/performanceMonitor/PerformanceMonitor.ts +4 -13
- package/src/performanceMonitor/formatPerformanceFactorsDebug.ts +2 -2
- package/src/performanceMonitor/index.ts +1 -5
- package/src/performanceMonitor/types.ts +1 -1
- package/src/playerState/playerState.ts +55 -50
- package/src/playerState/types.ts +2 -8
- package/src/playground/allEntitiesDebug.ts +19 -16
- package/src/playground/baseScene.ts +29 -32
- package/src/playground/mobileControls.tsx +133 -129
- package/src/playground/playground.html +76 -73
- package/src/playground/playground.ts +14 -1
- package/src/playground/playgroundUi.tsx +100 -74
- package/src/playground/reactUtils.ts +3 -7
- package/src/playground/scenes/entities.ts +10 -7
- package/src/playground/scenes/frequentUpdates.ts +5 -5
- package/src/playground/scenes/geometryExport.ts +6 -20
- package/src/playground/scenes/highPosEntityUpdates.ts +2 -10
- package/src/playground/scenes/index.ts +1 -1
- package/src/playground/scenes/lightingStarfield.ts +18 -12
- package/src/playground/scenes/main.ts +15 -11
- package/src/playground/scenes/partialBlockCulling.ts +31 -0
- package/src/playground/scenes/railsCobweb.ts +1 -1
- package/src/playground/scenes/rotationIssue.ts +1 -1
- package/src/playground/scenes/slabsOptimization.ts +1 -2
- package/src/playground/scenes/transparencyIssue.ts +1 -1
- package/src/playground/shared.ts +2 -2
- package/src/resourcesManager/resourcesManager.ts +29 -54
- package/src/resourcesManager/resourcesManager.worker.test.ts +6 -10
- package/src/shims/minecraftData.ts +70 -24
- package/src/sign-renderer/index.html +10 -12
- package/src/sign-renderer/index.ts +29 -29
- package/src/sign-renderer/playground.ts +22 -15
- package/src/sign-renderer/tests.test.ts +16 -13
- package/src/sign-renderer/vite.config.ts +2 -2
- package/src/three/appShared.ts +46 -25
- package/src/three/bannerRenderer.ts +33 -61
- package/src/three/cameraCollisionBlockCache.ts +156 -0
- package/src/three/cameraCollisionSolidity.ts +50 -0
- package/src/three/cameraShake.ts +15 -6
- package/src/three/chunkMeshManager.ts +223 -390
- package/src/three/cinimaticScript.ts +26 -15
- package/src/three/cubeDrawSpans.ts +27 -37
- package/src/three/cubeMultiDraw.ts +9 -22
- package/src/three/documentRenderer.ts +15 -18
- package/src/three/entities.ts +175 -150
- package/src/three/entity/EntityMesh.ts +114 -108
- package/src/three/entity/animations.js +3 -3
- package/src/three/entity/armorModels.ts +15 -15
- package/src/three/entity/gltfAnimationUtils.ts +2 -5
- package/src/three/fireworks.ts +34 -32
- package/src/three/fireworksRenderer.ts +32 -29
- package/src/three/globalBlockBuffer.ts +167 -100
- package/src/three/globalLegacyBuffer.ts +248 -264
- package/src/three/graphicsBackendBase.ts +19 -25
- package/src/three/graphicsBackendOffThread.ts +12 -15
- package/src/three/hand.ts +21 -21
- package/src/three/handLegacy.ts +3 -13
- package/src/three/holdingBlock.ts +116 -79
- package/src/three/holdingBlockFactory.ts +1 -4
- package/src/three/holdingBlockItemIdentity.test.ts +25 -17
- package/src/three/holdingBlockItemIdentity.ts +9 -6
- package/src/three/holdingBlockLegacy.ts +113 -132
- package/src/three/itemMesh.ts +20 -29
- package/src/three/legacyMultiDraw.ts +10 -16
- package/src/three/legacySectionCull.ts +8 -14
- package/src/three/menuBackground/activeView.ts +1 -4
- package/src/three/menuBackground/assetUrl.ts +1 -1
- package/src/three/menuBackground/classic.ts +4 -12
- package/src/three/menuBackground/gpuPreference.ts +1 -3
- package/src/three/menuBackground/index.ts +2 -11
- package/src/three/menuBackground/renderer.ts +1 -5
- package/src/three/menuBackground/types.ts +1 -4
- package/src/three/menuBackground/v2.ts +196 -96
- package/src/three/menuBackground/v2Meta.ts +3 -3
- package/src/three/menuBackground/worldBlocks.ts +37 -35
- package/src/three/modules/blockBreakParticles.ts +59 -28
- package/src/three/modules/cameraBobbing.ts +3 -6
- package/src/three/modules/index.ts +1 -1
- package/src/three/modules/rain.ts +10 -23
- package/src/three/modules/sciFiWorldReveal.ts +11 -25
- package/src/three/modules/starfield.ts +7 -13
- package/src/three/renderSlot.ts +28 -16
- package/src/three/rendererModuleSystem.ts +1 -3
- package/src/three/sceneOrigin.ts +96 -33
- package/src/three/sectionRaycastAabb.ts +24 -110
- package/src/three/shaderCubeMesh.ts +6 -17
- package/src/three/shaders/cubeBlockShader.ts +85 -96
- package/src/three/shaders/legacyBlockShader.ts +21 -43
- package/src/three/shaders/textureIndexMapping.ts +89 -95
- package/src/three/shaders/tintPalette.ts +156 -170
- package/src/three/signTextureCache.ts +9 -12
- package/src/three/skyboxRenderer.ts +62 -70
- package/src/three/tests/chunkMeshManagerLegacy.test.ts +34 -64
- package/src/three/tests/cubeDrawSpans.test.ts +55 -25
- package/src/three/tests/globalBlockBuffer.test.ts +217 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +300 -41
- package/src/three/tests/legacyMultiDraw.test.ts +8 -29
- package/src/three/tests/pr3PerFrameCpuCuts.test.ts +106 -29
- package/src/three/tests/signTextureCache.test.ts +4 -4
- package/src/three/thirdPersonVoxelRaycast.ts +120 -0
- package/src/three/threeJsMedia.ts +55 -72
- package/src/three/threeJsParticles.ts +32 -41
- package/src/three/threeJsSound.ts +3 -3
- package/src/three/threeJsUtils.ts +3 -3
- package/src/three/threeWorker.ts +2 -2
- package/src/three/waypointSprite.ts +55 -82
- package/src/three/waypoints.ts +20 -23
- package/src/three/world/cursorBlock.ts +25 -12
- package/src/three/world/vr.ts +11 -17
- package/src/three/worldGeometryExport.ts +15 -34
- package/src/three/worldRendererThree.ts +144 -166
- package/src/wasm-mesher/bridge/convertChunk.ts +3 -9
- package/src/wasm-mesher/bridge/render-from-wasm.ts +204 -229
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +26 -67
- package/src/wasm-mesher/tests/cameraCollisionBlockCache.test.ts +82 -0
- package/src/wasm-mesher/tests/cullingRegression.test.ts +307 -0
- package/src/wasm-mesher/tests/heightmapParity.test.ts +8 -18
- package/src/wasm-mesher/tests/mesherWasmConversionCache.test.ts +2 -2
- package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +1 -93
- package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +103 -141
- package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +14 -22
- package/src/wasm-mesher/tests/thirdPersonVoxelRaycast.test.ts +63 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +150 -141
- package/src/wasm-mesher/worker/mesherWasmConversionCache.ts +2 -8
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +1 -3
- package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +1 -5
- package/src/wasm-mesher/worker/mesherWasmRequestTracker.ts +42 -42
- package/src/worldView/worldView.spiral.test.ts +1 -1
- package/src/worldView/worldView.ts +34 -39
- package/src/worldView/worldViewWorkerBridge.test.ts +4 -4
|
@@ -28,7 +28,7 @@ const FACE_NAME_TO_INDEX: Record<string, number> = {
|
|
|
28
28
|
east: 2,
|
|
29
29
|
west: 3,
|
|
30
30
|
south: 4,
|
|
31
|
-
north: 5
|
|
31
|
+
north: 5
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -42,17 +42,13 @@ export const AO_LIGHT_REMAP: readonly (readonly number[])[] = [
|
|
|
42
42
|
[0, 1, 2, 3], // EAST
|
|
43
43
|
[0, 1, 2, 3], // WEST
|
|
44
44
|
[2, 3, 0, 1], // SOUTH
|
|
45
|
-
[2, 3, 0, 1]
|
|
45
|
+
[2, 3, 0, 1] // NORTH
|
|
46
46
|
] as const
|
|
47
47
|
|
|
48
48
|
/** Reorder per-corner AO or light values for shader-space corners. */
|
|
49
|
-
export function remapCornersForShaderFace(
|
|
50
|
-
faceIdx: number,
|
|
51
|
-
values: number[],
|
|
52
|
-
fallback: number,
|
|
53
|
-
): number[] {
|
|
49
|
+
export function remapCornersForShaderFace(faceIdx: number, values: number[], fallback: number): number[] {
|
|
54
50
|
const map = AO_LIGHT_REMAP[faceIdx] ?? AO_LIGHT_REMAP[0]
|
|
55
|
-
return map.map(
|
|
51
|
+
return map.map(i => values[i] ?? fallback)
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
export interface ShaderCubeBlockInput {
|
|
@@ -77,8 +73,8 @@ export interface ShaderCubeModelInput {
|
|
|
77
73
|
y?: number
|
|
78
74
|
z?: number
|
|
79
75
|
elements?: Array<{
|
|
80
|
-
rotation?: { axis: string
|
|
81
|
-
faces?: Record<string, { texture?: TextureEntry & { rotation?: number }
|
|
76
|
+
rotation?: { axis: string; angle: number; origin: number[] }
|
|
77
|
+
faces?: Record<string, { texture?: TextureEntry & { rotation?: number }; tintindex?: number }>
|
|
82
78
|
}>
|
|
83
79
|
}
|
|
84
80
|
}
|
|
@@ -88,11 +84,7 @@ let textureIndexMapping: TextureIndexMapping | null = null
|
|
|
88
84
|
let tintsMissingWarned = false
|
|
89
85
|
|
|
90
86
|
/** Convert mc-assets texture scales (normalized or negative) to pixel tile size for index lookup. */
|
|
91
|
-
function normalizeTextureEntryForTileIndex(
|
|
92
|
-
tex: { u?: number, v?: number, su?: number, sv?: number },
|
|
93
|
-
atlasWidth: number,
|
|
94
|
-
tileSize: number,
|
|
95
|
-
): TextureEntry {
|
|
87
|
+
function normalizeTextureEntryForTileIndex(tex: { u?: number; v?: number; su?: number; sv?: number }, atlasWidth: number, tileSize: number): TextureEntry {
|
|
96
88
|
let u = tex.u ?? 0
|
|
97
89
|
let v = tex.v ?? 0
|
|
98
90
|
let su = tex.su ?? tileSize
|
|
@@ -109,20 +101,13 @@ function normalizeTextureEntryForTileIndex(
|
|
|
109
101
|
type FaceTextureRef = TextureEntry & { tileIndex?: number }
|
|
110
102
|
|
|
111
103
|
/** Prefer atlas `tileIndex` from block model (legacy path uses the same). */
|
|
112
|
-
export function resolveFaceTileIndex(
|
|
113
|
-
tex: FaceTextureRef,
|
|
114
|
-
texMapping: TextureIndexMapping,
|
|
115
|
-
): number {
|
|
104
|
+
export function resolveFaceTileIndex(tex: FaceTextureRef, texMapping: TextureIndexMapping): number {
|
|
116
105
|
const fromAtlas = tex.tileIndex
|
|
117
106
|
// tile 0 is a special/atlas-padding slot; use pixel fallback for block faces
|
|
118
107
|
if (typeof fromAtlas === 'number' && fromAtlas > 0 && fromAtlas < 4096) {
|
|
119
108
|
return fromAtlas
|
|
120
109
|
}
|
|
121
|
-
const entry = normalizeTextureEntryForTileIndex(
|
|
122
|
-
tex,
|
|
123
|
-
texMapping.getTilesPerRow() * 16,
|
|
124
|
-
16,
|
|
125
|
-
)
|
|
110
|
+
const entry = normalizeTextureEntryForTileIndex(tex, texMapping.getTilesPerRow() * 16, 16)
|
|
126
111
|
return texMapping.tileIndexFromTextureEntry(entry)
|
|
127
112
|
}
|
|
128
113
|
|
|
@@ -158,7 +143,7 @@ export function getShaderCubeResources(): {
|
|
|
158
143
|
height: latest.height,
|
|
159
144
|
tileSize: latest.tileSize ?? 16,
|
|
160
145
|
suSv: latest.suSv ?? 16,
|
|
161
|
-
textures: latest.textures ?? {}
|
|
146
|
+
textures: latest.textures ?? {}
|
|
162
147
|
})
|
|
163
148
|
}
|
|
164
149
|
return { tintPalette, textureIndexMapping }
|
|
@@ -181,7 +166,7 @@ export function isShaderCubeBlock(
|
|
|
181
166
|
cached: ShaderCubeModelInput & { isCube: boolean },
|
|
182
167
|
model: ShaderCubeModelInput['model'],
|
|
183
168
|
sectionHeight: number,
|
|
184
|
-
texMapping: TextureIndexMapping
|
|
169
|
+
texMapping: TextureIndexMapping
|
|
185
170
|
): boolean {
|
|
186
171
|
if (sectionHeight !== 16) return false
|
|
187
172
|
if (!cached.isCube) return false
|
|
@@ -214,14 +199,7 @@ export function isShaderCubeBlock(
|
|
|
214
199
|
return true
|
|
215
200
|
}
|
|
216
201
|
|
|
217
|
-
function packWord0(
|
|
218
|
-
lx: number,
|
|
219
|
-
ly: number,
|
|
220
|
-
lz: number,
|
|
221
|
-
faceId: number,
|
|
222
|
-
tintIndex: number,
|
|
223
|
-
ao: number[],
|
|
224
|
-
): number {
|
|
202
|
+
function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[]): number {
|
|
225
203
|
let w = 0
|
|
226
204
|
w |= (lx & 0xf) << WORD0.LX_SHIFT
|
|
227
205
|
w |= (ly & 0xf) << WORD0.LY_SHIFT
|
|
@@ -246,13 +224,7 @@ function biasedSectionIndex(sectionBaseCoord: number): number {
|
|
|
246
224
|
return (Math.floor(sectionBaseCoord / 16) + WORD3.SECTION_BIAS) & WORD3.SECTION_MASK
|
|
247
225
|
}
|
|
248
226
|
|
|
249
|
-
export function packWord2(
|
|
250
|
-
texIndex: number,
|
|
251
|
-
aoDiagonalFlip: boolean,
|
|
252
|
-
sectionBaseX: number,
|
|
253
|
-
sectionBaseY: number,
|
|
254
|
-
sectionBaseZ: number,
|
|
255
|
-
): number {
|
|
227
|
+
export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number): number {
|
|
256
228
|
let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
|
|
257
229
|
if (aoDiagonalFlip) {
|
|
258
230
|
w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
|
|
@@ -273,7 +245,7 @@ export function packWord3(sectionBaseX: number, sectionBaseZ: number): number {
|
|
|
273
245
|
}
|
|
274
246
|
|
|
275
247
|
/** Decode section base block coords from packed words (round-trip helper for tests). */
|
|
276
|
-
export function decodeSectionBaseFromWords(word2: number, word3: number): { x: number
|
|
248
|
+
export function decodeSectionBaseFromWords(word2: number, word3: number): { x: number; y: number; z: number } {
|
|
277
249
|
const sX = ((word3 & 0xffff) | (((word2 >>> WORD2.SECTION_X_HI_SHIFT) & 0x3f) << 16)) - WORD3.SECTION_BIAS
|
|
278
250
|
const sZ = (((word3 >>> 16) & 0xffff) | (((word2 >>> WORD2.SECTION_Z_HI_SHIFT) & 0x3f) << 16)) - WORD3.SECTION_BIAS
|
|
279
251
|
const sY = ((word2 >>> WORD2.SECTION_Y_SHIFT) & ((1 << WORD2.SECTION_Y_BITS) - 1)) - 4
|
|
@@ -290,16 +262,13 @@ export function unpackTexIndexFromWord2(word2: number): number {
|
|
|
290
262
|
return word2 & ((1 << WORD2.TEX_INDEX_BITS) - 1)
|
|
291
263
|
}
|
|
292
264
|
|
|
293
|
-
function packCornerLightByte
|
|
265
|
+
function packCornerLightByte(skyNorm: number, blockNorm: number): number {
|
|
294
266
|
const sky4 = Math.min(15, Math.round(Math.max(0, skyNorm) * 15))
|
|
295
267
|
const block4 = Math.min(15, Math.round(Math.max(0, blockNorm) * 15))
|
|
296
268
|
return ((sky4 << 4) | block4) & 0xff
|
|
297
269
|
}
|
|
298
270
|
|
|
299
|
-
function lightCombinedForFace(
|
|
300
|
-
block: ShaderCubeBlockInput,
|
|
301
|
-
faceDataIndex: number,
|
|
302
|
-
): number[] {
|
|
271
|
+
function lightCombinedForFace(block: ShaderCubeBlockInput, faceDataIndex: number): number[] {
|
|
303
272
|
const packed = block.light_combined?.[faceDataIndex]
|
|
304
273
|
if (packed && packed.length === 4) {
|
|
305
274
|
return packed
|
|
@@ -310,7 +279,7 @@ function lightCombinedForFace(
|
|
|
310
279
|
return sky.map((s, i) => packCornerLightByte(s ?? 1, blockL[i] ?? 0))
|
|
311
280
|
}
|
|
312
281
|
const floats = block.light_data?.[faceDataIndex] ?? [1, 1, 1, 1]
|
|
313
|
-
return floats.map(
|
|
282
|
+
return floats.map(f => packCornerLightByte(f, 0))
|
|
314
283
|
}
|
|
315
284
|
|
|
316
285
|
function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number> {
|
|
@@ -326,7 +295,7 @@ function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number>
|
|
|
326
295
|
}
|
|
327
296
|
|
|
328
297
|
export type BuildShaderCubeInstancesOpts = {
|
|
329
|
-
sectionOrigin: { x: number
|
|
298
|
+
sectionOrigin: { x: number; y: number; z: number }
|
|
330
299
|
sectionHeight: number
|
|
331
300
|
biome?: string
|
|
332
301
|
tintPalette: TintPalette
|
|
@@ -336,6 +305,8 @@ export type BuildShaderCubeInstancesOpts = {
|
|
|
336
305
|
* diagonal flip — matches legacy render-from-wasm path.
|
|
337
306
|
*/
|
|
338
307
|
doAO?: boolean
|
|
308
|
+
/** Bitmask of faces to skip (same indices as visible_faces: 0=up … 5=north). */
|
|
309
|
+
forceCullMask?: number
|
|
339
310
|
}
|
|
340
311
|
|
|
341
312
|
/**
|
|
@@ -347,16 +318,9 @@ export function tryBuildShaderCubeInstances(
|
|
|
347
318
|
cached: ShaderCubeModelInput & { isCube: boolean },
|
|
348
319
|
model: ShaderCubeModelInput['model'],
|
|
349
320
|
opts: BuildShaderCubeInstancesOpts,
|
|
350
|
-
words: number[]
|
|
321
|
+
words: number[]
|
|
351
322
|
): boolean {
|
|
352
|
-
const {
|
|
353
|
-
sectionOrigin,
|
|
354
|
-
sectionHeight,
|
|
355
|
-
biome,
|
|
356
|
-
tintPalette,
|
|
357
|
-
textureIndexMapping,
|
|
358
|
-
doAO = true,
|
|
359
|
-
} = opts
|
|
323
|
+
const { sectionOrigin, sectionHeight, biome, tintPalette, textureIndexMapping, doAO = true, forceCullMask = 0 } = opts
|
|
360
324
|
|
|
361
325
|
if (!isShaderCubeBlock(cached, model, sectionHeight, textureIndexMapping)) {
|
|
362
326
|
return false
|
|
@@ -375,6 +339,7 @@ export function tryBuildShaderCubeInstances(
|
|
|
375
339
|
for (const faceName of WASM_FACE_ORDER) {
|
|
376
340
|
const faceIdx = FACE_NAME_TO_INDEX[faceName]
|
|
377
341
|
if ((block.visible_faces & (1 << faceIdx)) === 0) continue
|
|
342
|
+
if ((forceCullMask & (1 << faceIdx)) !== 0) continue
|
|
378
343
|
|
|
379
344
|
const faceDataIndex = wasmFaceToDataIndex[faceIdx]
|
|
380
345
|
if (faceDataIndex === undefined) continue
|
|
@@ -404,20 +369,14 @@ export function tryBuildShaderCubeInstances(
|
|
|
404
369
|
aoDiagonalFlip = false
|
|
405
370
|
}
|
|
406
371
|
|
|
407
|
-
const tintIndex = tintPalette.getTintIndex(
|
|
408
|
-
eFace.tintindex,
|
|
409
|
-
cached.blockName,
|
|
410
|
-
cached.blockProps,
|
|
411
|
-
biome ?? 'plains',
|
|
412
|
-
)
|
|
372
|
+
const tintIndex = tintPalette.getTintIndex(eFace.tintindex, cached.blockName, cached.blockProps, biome ?? 'plains')
|
|
413
373
|
|
|
414
374
|
words.push(
|
|
415
375
|
packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
|
|
416
376
|
packWord1(lightCombined),
|
|
417
377
|
packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z),
|
|
418
|
-
packWord3(sectionOrigin.x, sectionOrigin.z)
|
|
378
|
+
packWord3(sectionOrigin.x, sectionOrigin.z)
|
|
419
379
|
)
|
|
420
|
-
|
|
421
380
|
}
|
|
422
381
|
|
|
423
382
|
return true
|
|
@@ -429,7 +388,7 @@ export function buildShaderCubesFromWords(wordQuads: number[]): ShaderCubesOutpu
|
|
|
429
388
|
return {
|
|
430
389
|
words: new Uint32Array(wordQuads),
|
|
431
390
|
count: faceCount,
|
|
432
|
-
formatVersion: SHADER_CUBES_FORMAT_VERSION
|
|
391
|
+
formatVersion: SHADER_CUBES_FORMAT_VERSION
|
|
433
392
|
}
|
|
434
393
|
}
|
|
435
394
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect } from 'vitest'
|
|
3
|
+
import Chunks from 'prismarine-chunk'
|
|
4
|
+
import MinecraftData from 'minecraft-data'
|
|
5
|
+
import { Vec3 } from 'vec3'
|
|
6
|
+
import { CAMERA_COLLISION_BYTES_PER_SECTION, CameraCollisionBlockCache } from '../../three/cameraCollisionBlockCache'
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.16.5'
|
|
9
|
+
|
|
10
|
+
function makeChunkWithStoneFloor(y: number) {
|
|
11
|
+
const mcData = MinecraftData(VERSION)
|
|
12
|
+
const Chunk = Chunks(VERSION) as any
|
|
13
|
+
const chunk = new Chunk(undefined as any)
|
|
14
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
15
|
+
for (let x = 0; x < 16; x++) {
|
|
16
|
+
for (let z = 0; z < 16; z++) {
|
|
17
|
+
chunk.setBlockStateId(new Vec3(x, y, z), stoneId)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return chunk
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('CameraCollisionBlockCache: ingestColumn sets solids and skips air sections', () => {
|
|
24
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
25
|
+
cache.setWorldBounds(0, 256)
|
|
26
|
+
const chunk = makeChunkWithStoneFloor(64)
|
|
27
|
+
cache.ingestColumn(0, 0, chunk.toJson())
|
|
28
|
+
|
|
29
|
+
expect(cache.isSolidBlock(0, 64, 0)).toBe(true)
|
|
30
|
+
expect(cache.isSolidBlock(15, 64, 15)).toBe(true)
|
|
31
|
+
expect(cache.isSolidBlock(0, 65, 0)).toBe(false)
|
|
32
|
+
expect(cache.isSolidBlock(0, 0, 0)).toBe(false)
|
|
33
|
+
|
|
34
|
+
// One 16³ section at y=64 only (not the whole column height).
|
|
35
|
+
expect(cache.getAllocatedSectionCount()).toBe(1)
|
|
36
|
+
expect(cache.getAllocatedBytes()).toBe(CAMERA_COLLISION_BYTES_PER_SECTION)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('CameraCollisionBlockCache: setBlockStateId updates and frees empty sections', () => {
|
|
40
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
41
|
+
cache.setWorldBounds(0, 256)
|
|
42
|
+
const mcData = MinecraftData(VERSION)
|
|
43
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
44
|
+
|
|
45
|
+
cache.setBlockStateId(5, 70, 5, stoneId)
|
|
46
|
+
expect(cache.isSolidBlock(5, 70, 5)).toBe(true)
|
|
47
|
+
expect(cache.getAllocatedSectionCount()).toBe(1)
|
|
48
|
+
|
|
49
|
+
cache.setBlockStateId(5, 70, 5, 0)
|
|
50
|
+
expect(cache.isSolidBlock(5, 70, 5)).toBe(false)
|
|
51
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('CameraCollisionBlockCache: removeColumn frees all section bitsets in column', () => {
|
|
55
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
56
|
+
cache.setWorldBounds(0, 256)
|
|
57
|
+
const mcData = MinecraftData(VERSION)
|
|
58
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
59
|
+
|
|
60
|
+
cache.setBlockStateId(0, 64, 0, stoneId)
|
|
61
|
+
cache.setBlockStateId(0, 80, 0, stoneId)
|
|
62
|
+
expect(cache.getAllocatedSectionCount()).toBe(2)
|
|
63
|
+
|
|
64
|
+
cache.removeColumn(0, 0)
|
|
65
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
66
|
+
expect(cache.isSolidBlock(0, 64, 0)).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('CameraCollisionBlockCache: clear and setWorldBounds drop all sections', () => {
|
|
70
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
71
|
+
cache.setWorldBounds(0, 256)
|
|
72
|
+
const chunk = makeChunkWithStoneFloor(10)
|
|
73
|
+
cache.ingestColumn(16, 32, chunk.toJson())
|
|
74
|
+
expect(cache.getAllocatedSectionCount()).toBeGreaterThan(0)
|
|
75
|
+
|
|
76
|
+
cache.clear()
|
|
77
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
78
|
+
|
|
79
|
+
cache.ingestColumn(16, 32, chunk.toJson())
|
|
80
|
+
cache.setWorldBounds(-64, 320)
|
|
81
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
82
|
+
})
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect, beforeAll, beforeEach } 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 { World } from '../../mesher-shared/world'
|
|
13
|
+
import { setBlockStatesData, getSectionGeometry } from '../../mesher-shared/models'
|
|
14
|
+
import { resetFaceOcclusionCache } from '../../mesher-shared/faceOcclusion'
|
|
15
|
+
import { convertChunkToWasm } from '../bridge/convertChunk'
|
|
16
|
+
import { renderWasmOutputToGeometry } from '../bridge/render-from-wasm'
|
|
17
|
+
|
|
18
|
+
const VERSION = '1.18.2'
|
|
19
|
+
const SECTION_Y = 0
|
|
20
|
+
const SECTION_HEIGHT = 16
|
|
21
|
+
|
|
22
|
+
const DRY_STAIR_PROPS = { facing: 'east', half: 'bottom', shape: 'straight', waterlogged: false } as const
|
|
23
|
+
|
|
24
|
+
type BlockSpec = { x: number; y: number; z: number; name: string; props?: Record<string, string | boolean> }
|
|
25
|
+
|
|
26
|
+
let wasmModule: typeof import('../runtime-build/wasm_mesher.js')
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
wasmModule = await import('../runtime-build/wasm_mesher.js')
|
|
30
|
+
const wasmDir = dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
const wasmBytes = readFileSync(join(wasmDir, '../runtime-build/wasm_mesher_bg.wasm'))
|
|
32
|
+
wasmModule.initSync(wasmBytes)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
resetFaceOcclusionCache()
|
|
37
|
+
const mcData = MinecraftData(VERSION)
|
|
38
|
+
setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
|
|
39
|
+
;(globalThis as any).__wasmBlockModelCache = new Map()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function resolveStateId(mcData: ReturnType<typeof MinecraftData>, name: string, props?: Record<string, string | boolean>) {
|
|
43
|
+
const block = mcData.blocksByName[name]
|
|
44
|
+
if (!block) throw new Error(`Unknown block: ${name}`)
|
|
45
|
+
if (!props || Object.keys(props).length === 0) return block.defaultState
|
|
46
|
+
const Block = PrismarineBlockLoader(VERSION)
|
|
47
|
+
const requested = { ...props }
|
|
48
|
+
if (!('waterlogged' in requested)) requested.waterlogged = false
|
|
49
|
+
|
|
50
|
+
const matches: number[] = []
|
|
51
|
+
for (let id = block.minStateId; id <= block.maxStateId; id++) {
|
|
52
|
+
const stateProps = Block.fromStateId(id, 1).getProperties() as Record<string, string | boolean>
|
|
53
|
+
let match = true
|
|
54
|
+
for (const [key, val] of Object.entries(requested)) {
|
|
55
|
+
if (stateProps[key] !== val) {
|
|
56
|
+
match = false
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (match) matches.push(id)
|
|
61
|
+
}
|
|
62
|
+
if (matches.length === 0) throw new Error(`No state for ${name} ${JSON.stringify(props)}`)
|
|
63
|
+
return matches[0]!
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildWorld(blocks: BlockSpec[]): World {
|
|
67
|
+
const mcData = MinecraftData(VERSION)
|
|
68
|
+
const Chunk = Chunks(VERSION) as any
|
|
69
|
+
const chunk = new Chunk(undefined as any)
|
|
70
|
+
|
|
71
|
+
for (const b of blocks) {
|
|
72
|
+
const id = resolveStateId(mcData, b.name, b.props)
|
|
73
|
+
chunk.setBlockStateId(new Vec3(b.x, b.y, b.z), id)
|
|
74
|
+
chunk.setBlockLight(new Vec3(b.x, b.y, b.z), 15)
|
|
75
|
+
chunk.setSkyLight(new Vec3(b.x, b.y, b.z), 15)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const world = new World(VERSION)
|
|
79
|
+
world.addColumn(0, 0, chunk.toJson())
|
|
80
|
+
return world
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function countQuadsFromLegacy(world: World): number {
|
|
84
|
+
const geo = getSectionGeometry(0, SECTION_Y, 0, world, SECTION_HEIGHT)
|
|
85
|
+
const opaque = geo.indicesCount / 6
|
|
86
|
+
const blend = geo.blend ? geo.blend.indices.length / 6 : 0
|
|
87
|
+
return opaque + blend
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function countQuadsFromWasm(world: World, shaderCubes = false): number {
|
|
91
|
+
const column = world.getColumn(0, 0)!
|
|
92
|
+
const conversion = convertChunkToWasm(column, VERSION, 0, 0, SECTION_Y, SECTION_Y + SECTION_HEIGHT, SECTION_Y, SECTION_HEIGHT)
|
|
93
|
+
const wasmResult = wasmModule.generate_geometry(
|
|
94
|
+
0,
|
|
95
|
+
SECTION_Y,
|
|
96
|
+
0,
|
|
97
|
+
SECTION_HEIGHT,
|
|
98
|
+
SECTION_Y,
|
|
99
|
+
SECTION_Y + SECTION_HEIGHT,
|
|
100
|
+
SECTION_Y,
|
|
101
|
+
conversion.blockStates,
|
|
102
|
+
conversion.blockLight,
|
|
103
|
+
conversion.skyLight,
|
|
104
|
+
conversion.biomesArray,
|
|
105
|
+
conversion.invisibleBlocks,
|
|
106
|
+
conversion.transparentBlocks,
|
|
107
|
+
conversion.noAoBlocks,
|
|
108
|
+
conversion.cullIdenticalBlocks,
|
|
109
|
+
conversion.occludingBlocks,
|
|
110
|
+
true,
|
|
111
|
+
false,
|
|
112
|
+
15
|
|
113
|
+
)
|
|
114
|
+
const section = renderWasmOutputToGeometry(wasmResult, VERSION, '0,0,0', { x: 8, y: 8, z: 8 }, world, {
|
|
115
|
+
sectionHeight: SECTION_HEIGHT,
|
|
116
|
+
shaderCubes
|
|
117
|
+
})
|
|
118
|
+
const opaque = section.geometry.indices.length / 6
|
|
119
|
+
const blend = section.blendGeometry?.indices.length ? section.blendGeometry.indices.length / 6 : 0
|
|
120
|
+
const shader = shaderCubes ? (section.shaderCubes?.count ?? 0) : 0
|
|
121
|
+
return opaque + blend + shader
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function assertMesherParity(world: World, expectedQuads: number) {
|
|
125
|
+
const legacy = countQuadsFromLegacy(world)
|
|
126
|
+
const wasmLegacy = countQuadsFromWasm(world, false)
|
|
127
|
+
const wasmShader = countQuadsFromWasm(world, true)
|
|
128
|
+
expect(legacy).toBe(expectedQuads)
|
|
129
|
+
expect(wasmLegacy).toBe(expectedQuads)
|
|
130
|
+
expect(wasmShader).toBe(expectedQuads)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function farmlandFieldBlocks(): BlockSpec[] {
|
|
134
|
+
const blocks: BlockSpec[] = []
|
|
135
|
+
for (let z = 0; z < 16; z++) {
|
|
136
|
+
for (let x = 0; x < 16; x++) {
|
|
137
|
+
blocks.push({ x, y: 4, z, name: 'dirt' })
|
|
138
|
+
blocks.push({ x, y: 5, z, name: 'farmland' })
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return blocks
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function slabFieldBlocks(): BlockSpec[] {
|
|
145
|
+
const blocks: BlockSpec[] = []
|
|
146
|
+
for (let z = 0; z < 4; z++) {
|
|
147
|
+
for (let x = 0; x < 4; x++) {
|
|
148
|
+
blocks.push({ x, y: 0, z, name: 'stone' })
|
|
149
|
+
blocks.push({ x, y: 1, z, name: 'stone_slab' })
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return blocks
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function cutCopperStairsRunBlocks(): BlockSpec[] {
|
|
156
|
+
const blocks: BlockSpec[] = []
|
|
157
|
+
for (let x = 0; x < 8; x++) {
|
|
158
|
+
blocks.push({ x, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
|
|
159
|
+
blocks.push({ x, y: 0, z: 1, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
|
|
160
|
+
}
|
|
161
|
+
return blocks
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function cutCopperStairsAscentBlocks(): BlockSpec[] {
|
|
165
|
+
const blocks: BlockSpec[] = []
|
|
166
|
+
for (let x = 0; x < 6; x++) {
|
|
167
|
+
blocks.push({ x, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
|
|
168
|
+
blocks.push({ x, y: 1, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
|
|
169
|
+
}
|
|
170
|
+
return blocks
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function cutCopperStairsWestRunBlocks(): BlockSpec[] {
|
|
174
|
+
const blocks: BlockSpec[] = []
|
|
175
|
+
for (let x = 0; x < 8; x++) {
|
|
176
|
+
blocks.push({
|
|
177
|
+
x,
|
|
178
|
+
y: 0,
|
|
179
|
+
z: 0,
|
|
180
|
+
name: 'cut_copper_stairs',
|
|
181
|
+
props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false }
|
|
182
|
+
})
|
|
183
|
+
blocks.push({
|
|
184
|
+
x,
|
|
185
|
+
y: 0,
|
|
186
|
+
z: 1,
|
|
187
|
+
name: 'cut_copper_stairs',
|
|
188
|
+
props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false }
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
return blocks
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function cutCopperStairsEastWestMirrorBlocks(): BlockSpec[] {
|
|
195
|
+
const blocks: BlockSpec[] = []
|
|
196
|
+
for (let x = 0; x < 3; x++) {
|
|
197
|
+
blocks.push({ x, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } })
|
|
198
|
+
}
|
|
199
|
+
for (let x = 0; x < 3; x++) {
|
|
200
|
+
blocks.push({
|
|
201
|
+
x: x + 4,
|
|
202
|
+
y: 0,
|
|
203
|
+
z: 0,
|
|
204
|
+
name: 'cut_copper_stairs',
|
|
205
|
+
props: { facing: 'west', half: 'bottom', shape: 'straight', waterlogged: false }
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
return blocks
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function countQuadsForFacing(facing: 'east' | 'west' | 'south' | 'north', half: 'bottom' | 'top'): number {
|
|
212
|
+
const world = buildWorld([{ x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { facing, half, shape: 'straight', waterlogged: false } }])
|
|
213
|
+
return countQuadsFromWasm(world)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function glassLeavesClusterBlocks(): BlockSpec[] {
|
|
217
|
+
return [
|
|
218
|
+
{ x: 0, y: 0, z: 0, name: 'stone' },
|
|
219
|
+
{ x: 1, y: 0, z: 0, name: 'glass' },
|
|
220
|
+
{ x: 0, y: 0, z: 1, name: 'oak_leaves' },
|
|
221
|
+
{ x: 1, y: 0, z: 1, name: 'glass' }
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
test('culling regression: farmland field — internal side faces culled (pre-fix ~8704 quads)', () => {
|
|
226
|
+
const world = buildWorld(farmlandFieldBlocks())
|
|
227
|
+
// Post-fix: 640 quads (legacy + WASM agree). Pre-fix was ~8704 with all internal farmland sides drawn.
|
|
228
|
+
assertMesherParity(world, 640)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('culling regression: slab field', () => {
|
|
232
|
+
const world = buildWorld(slabFieldBlocks())
|
|
233
|
+
assertMesherParity(world, 64)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('culling regression: single cut copper stair legacy vs wasm', () => {
|
|
237
|
+
const world = buildWorld([{ x: 0, y: 0, z: 0, name: 'cut_copper_stairs', props: { ...DRY_STAIR_PROPS } }])
|
|
238
|
+
assertMesherParity(world, 11)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('culling regression: cut copper stairs run (east-facing)', () => {
|
|
242
|
+
const world = buildWorld(cutCopperStairsRunBlocks())
|
|
243
|
+
assertMesherParity(world, 116)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
test('culling regression: cut copper stairs run (west-facing, rotated)', () => {
|
|
247
|
+
const world = buildWorld(cutCopperStairsWestRunBlocks())
|
|
248
|
+
assertMesherParity(world, 116)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
test('culling regression: cut copper stairs ascent', () => {
|
|
252
|
+
const world = buildWorld(cutCopperStairsAscentBlocks())
|
|
253
|
+
assertMesherParity(world, 106)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('culling regression: 3 east + 3 west stairs mirror user scenario', () => {
|
|
257
|
+
const world = buildWorld(cutCopperStairsEastWestMirrorBlocks())
|
|
258
|
+
const legacy = countQuadsFromLegacy(world)
|
|
259
|
+
const wasmLegacy = countQuadsFromWasm(world, false)
|
|
260
|
+
const wasmShader = countQuadsFromWasm(world, true)
|
|
261
|
+
expect(legacy).toBe(wasmLegacy)
|
|
262
|
+
expect(legacy).toBe(wasmShader)
|
|
263
|
+
// 6 stairs with 2 internal interfaces culled per row; gap at x=3 prevents east↔west culling
|
|
264
|
+
expect(wasmShader).toBe(58)
|
|
265
|
+
expect(wasmShader).toBeLessThan(3 * 11 * 2)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
test('culling regression: all stair facings and top half match east baseline', () => {
|
|
269
|
+
const east = countQuadsForFacing('east', 'bottom')
|
|
270
|
+
expect(countQuadsForFacing('west', 'bottom')).toBe(east)
|
|
271
|
+
expect(countQuadsForFacing('south', 'bottom')).toBe(east)
|
|
272
|
+
expect(countQuadsForFacing('north', 'bottom')).toBe(east)
|
|
273
|
+
expect(countQuadsForFacing('east', 'top')).toBe(east)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
test('culling regression: glass/leaves cluster — see-through blocks not over-culled', () => {
|
|
277
|
+
const world = buildWorld(glassLeavesClusterBlocks())
|
|
278
|
+
// Leaves never occlude; glass self-culls. Baseline locks parity (pre shape-cull guard was 24 legacy-only).
|
|
279
|
+
assertMesherParity(world, 20)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
test('shader cubes: dirt UP face culled under farmland', () => {
|
|
283
|
+
const world = buildWorld([
|
|
284
|
+
{ x: 0, y: 0, z: 0, name: 'dirt' },
|
|
285
|
+
{ x: 0, y: 1, z: 0, name: 'farmland' }
|
|
286
|
+
])
|
|
287
|
+
assertMesherParity(world, 10)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('shader cubes: dirt DOWN face culled under top slab', () => {
|
|
291
|
+
const world = buildWorld([
|
|
292
|
+
{ x: 0, y: 0, z: 0, name: 'stone_slab', props: { type: 'top' } },
|
|
293
|
+
{ x: 0, y: 1, z: 0, name: 'dirt' }
|
|
294
|
+
])
|
|
295
|
+
assertMesherParity(world, 10)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
test('shader cubes: dirt side not culled beside bottom slab', () => {
|
|
299
|
+
const world = buildWorld([
|
|
300
|
+
{ x: 0, y: 0, z: 0, name: 'dirt' },
|
|
301
|
+
{ x: 1, y: 0, z: 0, name: 'stone_slab', props: { type: 'bottom' } }
|
|
302
|
+
])
|
|
303
|
+
const legacy = countQuadsFromLegacy(world)
|
|
304
|
+
const wasmShader = countQuadsFromWasm(world, true)
|
|
305
|
+
expect(wasmShader).toBe(legacy)
|
|
306
|
+
expect(wasmShader).toBeGreaterThan(5)
|
|
307
|
+
})
|
|
@@ -37,9 +37,9 @@ import { extractColumnHeightmap, WasmGeometryOutput } from '../bridge/render-fro
|
|
|
37
37
|
|
|
38
38
|
const VERSION = '1.16.5'
|
|
39
39
|
|
|
40
|
-
type BlockSpec = { x: number
|
|
40
|
+
type BlockSpec = { x: number; y: number; z: number; name: string }
|
|
41
41
|
|
|
42
|
-
function buildWorld(blocks: BlockSpec[]): { world: World
|
|
42
|
+
function buildWorld(blocks: BlockSpec[]): { world: World; invisibleStateIds: Set<number> } {
|
|
43
43
|
const mcData = MinecraftData(VERSION)
|
|
44
44
|
const Chunk = Chunks(VERSION) as any
|
|
45
45
|
const chunk = new Chunk(undefined as any)
|
|
@@ -74,12 +74,7 @@ function buildWorld(blocks: BlockSpec[]): { world: World, invisibleStateIds: Set
|
|
|
74
74
|
* Returns a plain `number[]` matching the on-the-wire shape of Rust's
|
|
75
75
|
* `Vec<i16>` (length 256, indexed `z*16+x`, sentinel `-32768`).
|
|
76
76
|
*/
|
|
77
|
-
function simulateRustColumnHeightmap(
|
|
78
|
-
world: World,
|
|
79
|
-
invisibleStateIds: Set<number>,
|
|
80
|
-
worldMinY: number,
|
|
81
|
-
worldMaxY: number
|
|
82
|
-
): number[] {
|
|
77
|
+
function simulateRustColumnHeightmap(world: World, invisibleStateIds: Set<number>, worldMinY: number, worldMaxY: number): number[] {
|
|
83
78
|
const heightmap = new Array<number>(256).fill(-32768)
|
|
84
79
|
const column = world.getColumn(0, 0)
|
|
85
80
|
if (!column) return heightmap
|
|
@@ -105,20 +100,15 @@ function makeWasmOutputWithHeightmap(heightmap: number[]): WasmGeometryOutput {
|
|
|
105
100
|
blocks: [],
|
|
106
101
|
block_count: 0,
|
|
107
102
|
block_iterations: 0,
|
|
108
|
-
heightmap
|
|
103
|
+
heightmap
|
|
109
104
|
}
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
function runParity(blocks: BlockSpec[]): { js: Int16Array
|
|
107
|
+
function runParity(blocks: BlockSpec[]): { js: Int16Array; rust: Int16Array } {
|
|
113
108
|
const { world, invisibleStateIds } = buildWorld(blocks)
|
|
114
109
|
const js = computeHeightmap(world, 0, 0)
|
|
115
110
|
|
|
116
|
-
const rustShaped = simulateRustColumnHeightmap(
|
|
117
|
-
world,
|
|
118
|
-
invisibleStateIds,
|
|
119
|
-
world.config.worldMinY,
|
|
120
|
-
world.config.worldMaxY
|
|
121
|
-
)
|
|
111
|
+
const rustShaped = simulateRustColumnHeightmap(world, invisibleStateIds, world.config.worldMinY, world.config.worldMaxY)
|
|
122
112
|
const wasmOutput = makeWasmOutputWithHeightmap(rustShaped)
|
|
123
113
|
const rust = extractColumnHeightmap(wasmOutput)
|
|
124
114
|
expect(rust).not.toBeNull()
|
|
@@ -156,7 +146,7 @@ test('heightmap parity: varied heights, every column populated (Rust == JS)', ()
|
|
|
156
146
|
blocks.push({ x: 2, y: 80, z: 0, name: 'oak_log' }) // top wins over y=64 below
|
|
157
147
|
blocks.push({ x: 2, y: 64, z: 0, name: 'stone' })
|
|
158
148
|
blocks.push({ x: 5, y: 255, z: 5, name: 'stone' }) // high-Y edge (worldMaxY-1)
|
|
159
|
-
blocks.push({ x: 6, y: 0, z: 6, name: 'stone' })
|
|
149
|
+
blocks.push({ x: 6, y: 0, z: 6, name: 'stone' }) // low-Y edge (worldMinY)
|
|
160
150
|
// Invisible block above a real surface must be skipped on both sides.
|
|
161
151
|
blocks.push({ x: 7, y: 10, z: 7, name: 'stone' })
|
|
162
152
|
blocks.push({ x: 7, y: 11, z: 7, name: 'barrier' })
|
|
@@ -172,7 +162,7 @@ test('heightmap parity: varied heights, every column populated (Rust == JS)', ()
|
|
|
172
162
|
expect(js[0 * 16 + 1]).toBe(70)
|
|
173
163
|
expect(js[0 * 16 + 2]).toBe(80)
|
|
174
164
|
expect(js[5 * 16 + 5]).toBe(255)
|
|
175
|
-
expect(js[6 * 16 + 6]).toBe(5)
|
|
165
|
+
expect(js[6 * 16 + 6]).toBe(5) // y=0 stone is below the y=5 floor stone
|
|
176
166
|
expect(js[7 * 16 + 7]).toBe(10) // barrier above must be skipped
|
|
177
167
|
expect(js[8 * 16 + 8]).toBe(12) // cave_air above must be skipped
|
|
178
168
|
})
|