minecraft-renderer 0.1.91 → 0.1.92
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 +76 -76
- package/dist/mesher.js.map +4 -4
- package/dist/mesherWasm.js +202 -202
- package/dist/minecraft-renderer.js +67 -67
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +424 -424
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +3 -0
- package/src/lib/worldrendererCommon.ts +4 -1
- package/src/mesher-shared/exportedGeometryTypes.ts +1 -0
- package/src/mesher-shared/models.ts +10 -0
- package/src/mesher-shared/occludingBlocks.ts +53 -0
- package/src/mesher-shared/shared.ts +2 -0
- package/src/mesher-shared/tests/visGraph.test.ts +62 -0
- package/src/mesher-shared/visGraph.ts +155 -0
- package/src/mesher-shared/visibilitySet.ts +124 -0
- package/src/three/chunkMeshManager.ts +103 -4
- package/src/three/entities.ts +4 -1
- package/src/three/globalBlockBuffer.ts +6 -0
- package/src/three/globalLegacyBuffer.ts +6 -0
- package/src/three/occlusion/sectionOcclusionCull.ts +90 -0
- package/src/three/occlusion/sectionOcclusionGraph.ts +272 -0
- package/src/three/tests/chunkMeshManagerLegacy.test.ts +7 -7
- package/src/three/tests/pr3PerFrameCpuCuts.test.ts +9 -9
- package/src/three/tests/sectionOcclusionCull.test.ts +51 -0
- package/src/three/tests/sectionOcclusionGraph.test.ts +134 -0
- package/src/three/worldRendererThree.ts +30 -6
- package/src/wasm-mesher/bridge/convertChunk.ts +5 -51
- package/src/wasm-mesher/tests/sectionVisibilityFromWorld.test.ts +108 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +13 -2
|
@@ -217,16 +217,16 @@ test('updateSectionCullAndSort: same visible set skips span rebuild on camera mo
|
|
|
217
217
|
const updateDrawSpansSpy = vi.spyOn(blendBuffer, 'updateDrawSpans')
|
|
218
218
|
|
|
219
219
|
const camera1 = makeCamera(8, 8, 20)
|
|
220
|
-
manager.updateSectionCullAndSort(camera1, 8, 8, 20)
|
|
220
|
+
manager.updateSectionCullAndSort(camera1, 8, 8, 20, false)
|
|
221
221
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(1)
|
|
222
222
|
|
|
223
223
|
while (blendBuffer.hasPendingUploads()) blendBuffer.uploadDirtyRange()
|
|
224
224
|
|
|
225
|
-
manager.updateSectionCullAndSort(camera1, 8, 8, 20)
|
|
225
|
+
manager.updateSectionCullAndSort(camera1, 8, 8, 20, false)
|
|
226
226
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(2)
|
|
227
227
|
|
|
228
228
|
const camera2 = makeCamera(8, 8, 18)
|
|
229
|
-
manager.updateSectionCullAndSort(camera2, 8, 8, 18)
|
|
229
|
+
manager.updateSectionCullAndSort(camera2, 8, 8, 18, false)
|
|
230
230
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(2)
|
|
231
231
|
|
|
232
232
|
manager.cleanupSection(key)
|
|
@@ -242,14 +242,14 @@ test('updateSectionCullAndSort: layoutVersion change forces span rebuild', () =>
|
|
|
242
242
|
const updateDrawSpansSpy = vi.spyOn(blendBuffer, 'updateDrawSpans')
|
|
243
243
|
|
|
244
244
|
const camera = makeCamera(8, 8, 20)
|
|
245
|
-
manager.updateSectionCullAndSort(camera, 8, 8, 20)
|
|
245
|
+
manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
|
|
246
246
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(1)
|
|
247
247
|
|
|
248
248
|
blendBuffer.removeSection(key)
|
|
249
249
|
blendBuffer.addSection(key, makeBlendOnlyGeometry().blend!, 8, 8, 8)
|
|
250
250
|
;(manager as unknown as ManagerInternals).registerLegacyCullSection(key, 8, 8, 8)
|
|
251
251
|
|
|
252
|
-
manager.updateSectionCullAndSort(camera, 8, 8, 20)
|
|
252
|
+
manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
|
|
253
253
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(2)
|
|
254
254
|
|
|
255
255
|
manager.cleanupSection(key)
|
|
@@ -268,7 +268,7 @@ function simulateRenderCullGate(manager: ChunkMeshManager, camera: THREE.Perspec
|
|
|
268
268
|
}
|
|
269
269
|
manager.markCullDirtyIfBufferStateChanged()
|
|
270
270
|
if (manager.cullDirty) {
|
|
271
|
-
manager.updateSectionCullAndSort(camera, x, y, z)
|
|
271
|
+
manager.updateSectionCullAndSort(camera, x, y, z, false)
|
|
272
272
|
manager.clearCullDirty()
|
|
273
273
|
}
|
|
274
274
|
}
|
|
@@ -282,7 +282,7 @@ test('markCullDirtyIfBufferStateChanged: finalize layout bump marks cull dirty',
|
|
|
282
282
|
drainLegacyUploads(opaqueBuf)
|
|
283
283
|
|
|
284
284
|
const camera = makeCamera(8, 8, 20)
|
|
285
|
-
manager.updateSectionCullAndSort(camera, 8, 8, 20)
|
|
285
|
+
manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
|
|
286
286
|
manager.clearCullDirty()
|
|
287
287
|
|
|
288
288
|
opaqueBuf.addSection(
|
|
@@ -353,7 +353,7 @@ test('updateSectionCullAndSort: defrag finalize forces span rebuild with static
|
|
|
353
353
|
const updateDrawSpansSpy = vi.spyOn(opaqueBuf, 'updateDrawSpans')
|
|
354
354
|
const camera = makeCamera(8, 8, 20)
|
|
355
355
|
|
|
356
|
-
manager.updateSectionCullAndSort(camera, 8, 8, 20)
|
|
356
|
+
manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
|
|
357
357
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(1)
|
|
358
358
|
|
|
359
359
|
opaqueBuf.removeSection('1,0,0')
|
|
@@ -362,7 +362,7 @@ test('updateSectionCullAndSort: defrag finalize forces span rebuild with static
|
|
|
362
362
|
drainLegacyUploads(opaqueBuf)
|
|
363
363
|
opaqueBuf.compactStep()
|
|
364
364
|
|
|
365
|
-
manager.updateSectionCullAndSort(camera, 8, 8, 20)
|
|
365
|
+
manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
|
|
366
366
|
expect(updateDrawSpansSpy).toHaveBeenCalledTimes(2)
|
|
367
367
|
|
|
368
368
|
for (const key of keys) manager.cleanupSection(key)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect } from 'vitest'
|
|
3
|
+
import { SectionOcclusionCull } from '../occlusion/sectionOcclusionCull'
|
|
4
|
+
import { computeSectionVisibilitySet } from '../../mesher-shared/visGraph'
|
|
5
|
+
|
|
6
|
+
const ALL_OPEN = computeSectionVisibilitySet(16, () => false)
|
|
7
|
+
|
|
8
|
+
test('isSectionVisible fails open for unregistered sections', () => {
|
|
9
|
+
const cull = new SectionOcclusionCull()
|
|
10
|
+
cull.registerSection('0,0,0', ALL_OPEN, 8, 8, 8)
|
|
11
|
+
cull.update({
|
|
12
|
+
smartCull: true,
|
|
13
|
+
cameraWorldX: 8,
|
|
14
|
+
cameraWorldY: 8,
|
|
15
|
+
cameraWorldZ: 8,
|
|
16
|
+
viewDistance: 4,
|
|
17
|
+
sectionHeight: 16,
|
|
18
|
+
worldMinY: 0,
|
|
19
|
+
worldMaxY: 256
|
|
20
|
+
})
|
|
21
|
+
expect(cull.isSectionVisible('64,0,64')).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('notifySmartCullChanged invalidates graph', () => {
|
|
25
|
+
const cull = new SectionOcclusionCull()
|
|
26
|
+
cull.registerSection('0,0,0', ALL_OPEN, 8, 8, 8)
|
|
27
|
+
cull.update({
|
|
28
|
+
smartCull: false,
|
|
29
|
+
cameraWorldX: 8,
|
|
30
|
+
cameraWorldY: 8,
|
|
31
|
+
cameraWorldZ: 8,
|
|
32
|
+
viewDistance: 4,
|
|
33
|
+
sectionHeight: 16,
|
|
34
|
+
worldMinY: 0,
|
|
35
|
+
worldMaxY: 256
|
|
36
|
+
})
|
|
37
|
+
const offVisible = cull.getVisibleKeys()
|
|
38
|
+
expect(offVisible.has('0,0,0')).toBe(true)
|
|
39
|
+
cull.invalidate()
|
|
40
|
+
cull.update({
|
|
41
|
+
smartCull: true,
|
|
42
|
+
cameraWorldX: 8,
|
|
43
|
+
cameraWorldY: 8,
|
|
44
|
+
cameraWorldZ: 8,
|
|
45
|
+
viewDistance: 4,
|
|
46
|
+
sectionHeight: 16,
|
|
47
|
+
worldMinY: 0,
|
|
48
|
+
worldMaxY: 256
|
|
49
|
+
})
|
|
50
|
+
expect(cull.isSectionVisible('0,0,0')).toBe(true)
|
|
51
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect } from 'vitest'
|
|
3
|
+
import { Direction } from '../../mesher-shared/visibilitySet'
|
|
4
|
+
import { SectionOcclusionGraph } from '../occlusion/sectionOcclusionGraph'
|
|
5
|
+
import { computeSectionVisibilitySet } from '../../mesher-shared/visGraph'
|
|
6
|
+
|
|
7
|
+
const ALL_OPEN = computeSectionVisibilitySet(16, () => false)
|
|
8
|
+
const SOLID = computeSectionVisibilitySet(16, () => true)
|
|
9
|
+
|
|
10
|
+
function reg(graph: SectionOcclusionGraph, key: string, vis = ALL_OPEN) {
|
|
11
|
+
const [x, y, z] = key.split(',').map(Number)
|
|
12
|
+
graph.registerSection(key, { visibilitySet: vis, worldX: x!, worldY: y!, worldZ: z! })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('BFS from camera section reaches neighbors when smart cull on and faces connect', () => {
|
|
16
|
+
const graph = new SectionOcclusionGraph()
|
|
17
|
+
reg(graph, '0,0,0')
|
|
18
|
+
reg(graph, '16,0,0')
|
|
19
|
+
reg(graph, '-16,0,0')
|
|
20
|
+
|
|
21
|
+
const visible = graph.update({
|
|
22
|
+
smartCull: true,
|
|
23
|
+
cameraWorldX: 8,
|
|
24
|
+
cameraWorldY: 8,
|
|
25
|
+
cameraWorldZ: 8,
|
|
26
|
+
viewDistance: 4,
|
|
27
|
+
sectionHeight: 16,
|
|
28
|
+
worldMinY: 0,
|
|
29
|
+
worldMaxY: 256
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(visible.has('0,0,0')).toBe(true)
|
|
33
|
+
expect(visible.has('16,0,0')).toBe(true)
|
|
34
|
+
expect(visible.has('-16,0,0')).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('solid section blocks traversal to neighbor behind it', () => {
|
|
38
|
+
const graph = new SectionOcclusionGraph()
|
|
39
|
+
reg(graph, '0,0,0', ALL_OPEN)
|
|
40
|
+
reg(graph, '16,0,0', SOLID)
|
|
41
|
+
reg(graph, '32,0,0', ALL_OPEN)
|
|
42
|
+
|
|
43
|
+
const visible = graph.update({
|
|
44
|
+
smartCull: true,
|
|
45
|
+
cameraWorldX: 8,
|
|
46
|
+
cameraWorldY: 8,
|
|
47
|
+
cameraWorldZ: 8,
|
|
48
|
+
viewDistance: 4,
|
|
49
|
+
sectionHeight: 16,
|
|
50
|
+
worldMinY: 0,
|
|
51
|
+
worldMaxY: 256
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
expect(visible.has('0,0,0')).toBe(true)
|
|
55
|
+
expect(visible.has('16,0,0')).toBe(true)
|
|
56
|
+
expect(visible.has('32,0,0')).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('smart cull off shows all registered sections', () => {
|
|
60
|
+
const graph = new SectionOcclusionGraph()
|
|
61
|
+
reg(graph, '0,0,0')
|
|
62
|
+
reg(graph, '64,0,0', SOLID)
|
|
63
|
+
|
|
64
|
+
const visible = graph.update({
|
|
65
|
+
smartCull: false,
|
|
66
|
+
cameraWorldX: 8,
|
|
67
|
+
cameraWorldY: 8,
|
|
68
|
+
cameraWorldZ: 8,
|
|
69
|
+
viewDistance: 2,
|
|
70
|
+
sectionHeight: 16,
|
|
71
|
+
worldMinY: 0,
|
|
72
|
+
worldMaxY: 256
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(visible.has('64,0,0')).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('BFS assigns increasing step values', () => {
|
|
79
|
+
const graph = new SectionOcclusionGraph()
|
|
80
|
+
reg(graph, '0,0,0')
|
|
81
|
+
reg(graph, '16,0,0')
|
|
82
|
+
|
|
83
|
+
graph.update({
|
|
84
|
+
smartCull: true,
|
|
85
|
+
cameraWorldX: 8,
|
|
86
|
+
cameraWorldY: 8,
|
|
87
|
+
cameraWorldZ: 8,
|
|
88
|
+
viewDistance: 4,
|
|
89
|
+
sectionHeight: 16,
|
|
90
|
+
worldMinY: 0,
|
|
91
|
+
worldMaxY: 256
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(graph.getStep('0,0,0')).toBe(0)
|
|
95
|
+
expect(graph.getStep('16,0,0')).toBe(1)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('N-S tunnel section only connects through north/south faces', () => {
|
|
99
|
+
const tunnelVis = computeSectionVisibilitySet(16, (lx, _ly, _lz) => lx === 0 || lx === 15)
|
|
100
|
+
const graph = new SectionOcclusionGraph()
|
|
101
|
+
reg(graph, '0,0,0', tunnelVis)
|
|
102
|
+
reg(graph, '0,0,16', ALL_OPEN)
|
|
103
|
+
|
|
104
|
+
const visible = graph.update({
|
|
105
|
+
smartCull: true,
|
|
106
|
+
cameraWorldX: 8,
|
|
107
|
+
cameraWorldY: 8,
|
|
108
|
+
cameraWorldZ: 8,
|
|
109
|
+
viewDistance: 4,
|
|
110
|
+
sectionHeight: 16,
|
|
111
|
+
worldMinY: 0,
|
|
112
|
+
worldMaxY: 256
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(visible.has('0,0,16')).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('missing camera section seeds from surface ring', () => {
|
|
119
|
+
const graph = new SectionOcclusionGraph()
|
|
120
|
+
reg(graph, '0,0,0')
|
|
121
|
+
|
|
122
|
+
const visible = graph.update({
|
|
123
|
+
smartCull: true,
|
|
124
|
+
cameraWorldX: 8,
|
|
125
|
+
cameraWorldY: -32,
|
|
126
|
+
cameraWorldZ: 8,
|
|
127
|
+
viewDistance: 4,
|
|
128
|
+
sectionHeight: 16,
|
|
129
|
+
worldMinY: 0,
|
|
130
|
+
worldMaxY: 256
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
expect(visible.has('0,0,0')).toBe(true)
|
|
134
|
+
})
|
|
@@ -49,6 +49,25 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
49
49
|
protected override isShaderCubeBlocksEnabled(): boolean {
|
|
50
50
|
return this.worldRendererConfig.shaderCubeBlocks === true && !!this.renderer?.capabilities?.isWebGL2
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/** Section occlusion graph (smart / cave cull); forced off in spectator per issue #77. */
|
|
54
|
+
isSmartCullEnabled(): boolean {
|
|
55
|
+
return this.worldRendererConfig.smartCull !== false && this.reactiveDebugParams.smartCull !== false && this.playerStateReactive.gameMode !== 'spectator'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
isSectionOcclusionVisible(sectionKey: string): boolean {
|
|
59
|
+
if (!this.isSmartCullEnabled()) return true
|
|
60
|
+
return this.chunkMeshManager.isSectionOcclusionVisible(sectionKey)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
entitySectionKey(worldX: number, worldY: number, worldZ: number): string {
|
|
64
|
+
const sectionHeight = this.getSectionHeight()
|
|
65
|
+
const x = Math.floor(worldX / 16) * 16
|
|
66
|
+
const y = Math.floor(worldY / sectionHeight) * sectionHeight
|
|
67
|
+
const z = Math.floor(worldZ / 16) * 16
|
|
68
|
+
return `${x},${y},${z}`
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
chunkMeshManager: ChunkMeshManager
|
|
53
72
|
get sectionObjects() {
|
|
54
73
|
return this.chunkMeshManager.sectionObjects
|
|
@@ -787,6 +806,8 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
787
806
|
const s = gb.legacyBlend
|
|
788
807
|
text += `LEG-B: ${formatCompact(s.used)}/${formatCompact(s.capacity)}q `
|
|
789
808
|
}
|
|
809
|
+
const drawn = this.chunkMeshManager.getDrawnStats()
|
|
810
|
+
text += `DRAWN: ${formatCompact(drawn.cubeFaces)}f ${formatCompact(drawn.legacyOpaqueQuads)}q ${formatCompact(drawn.legacyBlendQuads)}q `
|
|
790
811
|
text += `MEM: ${this.chunkMeshManager.getEstimatedMemoryUsage().total} `
|
|
791
812
|
const camCollisionBytes = this.cameraCollisionBlockCache.getAllocatedBytes()
|
|
792
813
|
if (camCollisionBytes > 0) {
|
|
@@ -1285,11 +1306,6 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1285
1306
|
}
|
|
1286
1307
|
|
|
1287
1308
|
let entitiesRenderMs = 0
|
|
1288
|
-
if (!this.reactiveDebugParams.disableEntities) {
|
|
1289
|
-
const entitiesStart = performance.now()
|
|
1290
|
-
this.entities.render()
|
|
1291
|
-
entitiesRenderMs = performance.now() - entitiesStart
|
|
1292
|
-
}
|
|
1293
1309
|
|
|
1294
1310
|
// eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
|
|
1295
1311
|
const cam =
|
|
@@ -1340,10 +1356,18 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1340
1356
|
this.chunkMeshManager.markCullDirtyIfBufferStateChanged()
|
|
1341
1357
|
this.chunkMeshManager.setLegacyCameraOrigin(camX, camY, camZ)
|
|
1342
1358
|
this.chunkMeshManager.updateCullDirtyFromCamera(cam, camX, camY, camZ)
|
|
1359
|
+
const smartCull = this.isSmartCullEnabled()
|
|
1360
|
+
this.chunkMeshManager.notifySmartCullChanged(smartCull)
|
|
1343
1361
|
if (this.chunkMeshManager.cullDirty) {
|
|
1344
|
-
this.chunkMeshManager.updateSectionCullAndSort(cam, this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
|
|
1362
|
+
this.chunkMeshManager.updateSectionCullAndSort(cam, this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z, smartCull)
|
|
1345
1363
|
this.chunkMeshManager.clearCullDirty()
|
|
1346
1364
|
}
|
|
1365
|
+
this.chunkMeshManager.updateCaveCullingDebug(this.reactiveDebugParams.caveCullingDebug, smartCull)
|
|
1366
|
+
if (!this.reactiveDebugParams.disableEntities) {
|
|
1367
|
+
const entitiesStart = performance.now()
|
|
1368
|
+
this.entities.render()
|
|
1369
|
+
entitiesRenderMs = performance.now() - entitiesStart
|
|
1370
|
+
}
|
|
1347
1371
|
this.chunkMeshManager.sortVisibleBlendSections(camX, camY, camZ)
|
|
1348
1372
|
if (!didBlendAllAttrUpload && globalLegacyBlendBuffer?.hasPendingIndexUploads()) {
|
|
1349
1373
|
globalLegacyBlendBuffer.uploadDirtyIndexRange()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { Vec3 } from 'vec3'
|
|
3
3
|
import MinecraftData from 'minecraft-data'
|
|
4
|
-
import PrismarineBlockLoader from 'prismarine-block'
|
|
5
4
|
import moreBlockDataGeneratedJson from '../../lib/moreBlockDataGenerated.json'
|
|
5
|
+
import { getOccludingBlockMeta } from '../../mesher-shared/occludingBlocks'
|
|
6
6
|
|
|
7
7
|
type BlockMeta = {
|
|
8
8
|
invisibleBlocks: Uint16Array
|
|
@@ -10,6 +10,7 @@ type BlockMeta = {
|
|
|
10
10
|
noAoBlocks: Uint16Array
|
|
11
11
|
cullIdenticalBlocks: Uint16Array
|
|
12
12
|
occludingBlocks: Uint16Array
|
|
13
|
+
occludingLookup: Uint8Array
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const metaCache = new Map<string, BlockMeta>()
|
|
@@ -22,38 +23,6 @@ const blockToIds = (block: { minStateId: number; maxStateId: number }) => {
|
|
|
22
23
|
return ids
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const isCube = (shapes: any) => {
|
|
26
|
-
if (!shapes || shapes.length !== 1) return false
|
|
27
|
-
const s = shapes[0]
|
|
28
|
-
return s[0] === 0 && s[1] === 0 && s[2] === 0 && s[3] === 1 && s[4] === 1 && s[5] === 1
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const isLikelyFullCubeBlockName = (name: string) => {
|
|
32
|
-
if (!name) return false
|
|
33
|
-
if (name.includes('stairs')) return false
|
|
34
|
-
if (name.includes('slab')) return false
|
|
35
|
-
if (name.includes('fence')) return false
|
|
36
|
-
if (name.includes('gate')) return false
|
|
37
|
-
if (name.includes('pane')) return false
|
|
38
|
-
if (name.includes('wall')) return false
|
|
39
|
-
if (name.includes('door')) return false
|
|
40
|
-
if (name.includes('trapdoor')) return false
|
|
41
|
-
if (name.includes('sign')) return false
|
|
42
|
-
if (name.includes('banner')) return false
|
|
43
|
-
if (name.includes('rail')) return false
|
|
44
|
-
if (name.includes('torch')) return false
|
|
45
|
-
if (name.includes('lantern')) return false
|
|
46
|
-
if (name.includes('button')) return false
|
|
47
|
-
if (name.includes('lever')) return false
|
|
48
|
-
if (name.includes('pressure_plate')) return false
|
|
49
|
-
if (name.includes('carpet')) return false
|
|
50
|
-
if (name.includes('flower')) return false
|
|
51
|
-
if (name.includes('sapling')) return false
|
|
52
|
-
if (name.includes('tall_grass')) return false
|
|
53
|
-
if (name === 'grass' || name === 'short_grass' || name === 'tall_grass') return false
|
|
54
|
-
return true
|
|
55
|
-
}
|
|
56
|
-
|
|
57
26
|
export const getBlockMeta = (version: string): BlockMeta => {
|
|
58
27
|
const cached = metaCache.get(version)
|
|
59
28
|
if (cached) return cached
|
|
@@ -64,30 +33,15 @@ export const getBlockMeta = (version: string): BlockMeta => {
|
|
|
64
33
|
const transparentBlocks = new Uint16Array(mcData.blocksArray.filter(x => x.transparent).flatMap(blockToIds))
|
|
65
34
|
const noAoBlocks = new Uint16Array(mcData.blocksArray.filter(x => moreBlockDataGeneratedJson.noOcclusions[x.name]).flatMap(blockToIds))
|
|
66
35
|
const cullIdenticalBlocks = new Uint16Array(mcData.blocksArray.filter(x => x.name.includes('glass') || x.name.includes('ice')).flatMap(blockToIds))
|
|
67
|
-
const
|
|
68
|
-
const noOcclusionsSet = new Set(Object.keys(moreBlockDataGeneratedJson.noOcclusions))
|
|
69
|
-
|
|
70
|
-
const occludingBlockIds: number[] = []
|
|
71
|
-
for (const idStr of Object.keys((mcData as any).blocksByStateId)) {
|
|
72
|
-
const id = Number(idStr)
|
|
73
|
-
if (!id) continue
|
|
74
|
-
const b = (Block as any).fromStateId(id, 0)
|
|
75
|
-
if (!b) continue
|
|
76
|
-
if (b.transparent) continue
|
|
77
|
-
if (b.boundingBox !== 'block') continue
|
|
78
|
-
if (noOcclusionsSet.has(b.name)) continue
|
|
79
|
-
if (!isCube(b.shapes)) continue
|
|
80
|
-
occludingBlockIds.push(id)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const occludingBlocks = new Uint16Array(occludingBlockIds)
|
|
36
|
+
const { occludingBlocks, occludingLookup } = getOccludingBlockMeta(version)
|
|
84
37
|
|
|
85
38
|
const meta = {
|
|
86
39
|
invisibleBlocks,
|
|
87
40
|
transparentBlocks,
|
|
88
41
|
noAoBlocks,
|
|
89
42
|
cullIdenticalBlocks,
|
|
90
|
-
occludingBlocks
|
|
43
|
+
occludingBlocks,
|
|
44
|
+
occludingLookup
|
|
91
45
|
}
|
|
92
46
|
|
|
93
47
|
metaCache.set(version, meta)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect, beforeEach } from 'vitest'
|
|
3
|
+
import MinecraftData from 'minecraft-data'
|
|
4
|
+
import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
|
|
5
|
+
import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
|
|
6
|
+
import Chunks from 'prismarine-chunk'
|
|
7
|
+
import { Vec3 } from 'vec3'
|
|
8
|
+
import { setBlockStatesData } from '../../mesher-shared/models'
|
|
9
|
+
import { getOccludingBlockMeta } from '../../mesher-shared/occludingBlocks'
|
|
10
|
+
import { VisGraph, packVisibilitySet } from '../../mesher-shared/visGraph'
|
|
11
|
+
import { Direction, unpackVisibilitySet } from '../../mesher-shared/visibilitySet'
|
|
12
|
+
import { World } from '../../mesher-shared/world'
|
|
13
|
+
|
|
14
|
+
const VERSION = '1.18.2'
|
|
15
|
+
const SECTION_HEIGHT = 16
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const mcData = MinecraftData(VERSION)
|
|
19
|
+
setBlockStatesData(blockStatesModels, blocksAtlasesJson, false, true, VERSION, { blocks: mcData.blocksArray })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function buildVisibilityFromWorld(world: World, sx: number, sy: number, sz: number, sectionHeight: number): number {
|
|
23
|
+
const { occludingLookup } = getOccludingBlockMeta(VERSION)
|
|
24
|
+
const graph = new VisGraph()
|
|
25
|
+
const cursor = new Vec3(0, 0, 0)
|
|
26
|
+
for (cursor.y = sy; cursor.y < sy + sectionHeight; cursor.y++) {
|
|
27
|
+
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
|
28
|
+
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
|
29
|
+
const block = world.getBlock(cursor)
|
|
30
|
+
if (block && occludingLookup[block.stateId]) {
|
|
31
|
+
graph.setOpaque(cursor.x - sx, cursor.y - sy, cursor.z - sz)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return packVisibilitySet(graph.resolve())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function solidStoneSectionWorld(): World {
|
|
40
|
+
const mcData = MinecraftData(VERSION)
|
|
41
|
+
const stoneId = mcData.blocksByName.stone!.defaultState
|
|
42
|
+
const Chunk = Chunks(VERSION) as any
|
|
43
|
+
const chunk = new Chunk(undefined as any)
|
|
44
|
+
for (let y = 0; y < SECTION_HEIGHT; y++) {
|
|
45
|
+
for (let z = 0; z < 16; z++) {
|
|
46
|
+
for (let x = 0; x < 16; x++) {
|
|
47
|
+
chunk.setBlockStateId(new Vec3(x, y, z), stoneId)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const world = new World(VERSION)
|
|
52
|
+
world.addColumn(0, 0, chunk.toJson())
|
|
53
|
+
return world
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
test('occludingLookup VisGraph: solid 16³ interior packs no face pairs (not all-open)', () => {
|
|
57
|
+
const world = solidStoneSectionWorld()
|
|
58
|
+
const bits = buildVisibilityFromWorld(world, 0, 0, 0, SECTION_HEIGHT)
|
|
59
|
+
const vs = unpackVisibilitySet(bits)
|
|
60
|
+
for (let a = 0; a < 6; a++) {
|
|
61
|
+
for (let b = 0; b < 6; b++) {
|
|
62
|
+
if (a === b) continue
|
|
63
|
+
expect(vs.visibilityBetween(a, b)).toBe(false)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
expect(vs.visibilityBetween(Direction.NORTH, Direction.SOUTH)).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('occludingLookup VisGraph: empty section is all-open', () => {
|
|
70
|
+
const world = new World(VERSION)
|
|
71
|
+
const Chunk = Chunks(VERSION) as any
|
|
72
|
+
world.addColumn(0, 0, new Chunk(undefined as any).toJson())
|
|
73
|
+
const bits = buildVisibilityFromWorld(world, 0, 0, 0, SECTION_HEIGHT)
|
|
74
|
+
const vs = unpackVisibilitySet(bits)
|
|
75
|
+
expect(vs.visibilityBetween(Direction.NORTH, Direction.SOUTH)).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('occludingLookup VisGraph: tunnel N↔S only → only N/S connected', () => {
|
|
79
|
+
const mcData = MinecraftData(VERSION)
|
|
80
|
+
const stoneId = mcData.blocksByName.stone!.defaultState
|
|
81
|
+
const Chunk = Chunks(VERSION) as any
|
|
82
|
+
const chunk = new Chunk(undefined as any)
|
|
83
|
+
for (let y = 0; y < SECTION_HEIGHT; y++) {
|
|
84
|
+
for (let z = 0; z < 16; z++) {
|
|
85
|
+
for (let x = 0; x < 16; x++) {
|
|
86
|
+
if (x === 0 || x === 15) {
|
|
87
|
+
chunk.setBlockStateId(new Vec3(x, y, z), stoneId)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const world = new World(VERSION)
|
|
93
|
+
world.addColumn(0, 0, chunk.toJson())
|
|
94
|
+
const bits = buildVisibilityFromWorld(world, 0, 0, 0, SECTION_HEIGHT)
|
|
95
|
+
const vs = unpackVisibilitySet(bits)
|
|
96
|
+
expect(vs.visibilityBetween(Direction.NORTH, Direction.SOUTH)).toBe(true)
|
|
97
|
+
expect(vs.visibilityBetween(Direction.EAST, Direction.WEST)).toBe(false)
|
|
98
|
+
expect(vs.visibilityBetween(Direction.NORTH, Direction.EAST)).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('occludingLookup: stone is occluding, air is not', () => {
|
|
102
|
+
const mcData = MinecraftData(VERSION)
|
|
103
|
+
const { occludingLookup } = getOccludingBlockMeta(VERSION)
|
|
104
|
+
const stoneId = mcData.blocksByName.stone!.defaultState
|
|
105
|
+
const airId = mcData.blocksByName.air!.defaultState
|
|
106
|
+
expect(occludingLookup[stoneId]).toBe(1)
|
|
107
|
+
expect(occludingLookup[airId]).toBe(0)
|
|
108
|
+
})
|
|
@@ -4,6 +4,8 @@ import { convertChunkToWasm, getBlockMeta, type ChunkConversionResult } from '..
|
|
|
4
4
|
import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../bridge/render-from-wasm'
|
|
5
5
|
import { setBlockStatesData as setMesherData } from '../../mesher-shared/models'
|
|
6
6
|
import { defaultMesherConfig, type MesherGeometryOutput, SECTION_HEIGHT } from '../../mesher-shared/shared'
|
|
7
|
+
import { packVisibilitySet, VISIBILITY_SET_ALL_TRUE } from '../../mesher-shared/visibilitySet'
|
|
8
|
+
import { VisGraph } from '../../mesher-shared/visGraph'
|
|
7
9
|
import { worldColumnKey, World } from '../../mesher-shared/world'
|
|
8
10
|
import { handleGetHeightmap, EMPTY_COLUMN_HEIGHTMAP_SENTINEL } from '../../mesher-shared/computeHeightmap'
|
|
9
11
|
import { collectBlockEntityMetadata, type SignMeta, type HeadMeta, type BannerMeta } from '../../mesher-shared/blockEntityMetadata'
|
|
@@ -1077,7 +1079,8 @@ function makeEmptyColumnGeometry(sx: number, sy: number, sz: number, sectionHeig
|
|
|
1077
1079
|
signs: {},
|
|
1078
1080
|
banners: {},
|
|
1079
1081
|
hadErrors,
|
|
1080
|
-
blocksCount: 0
|
|
1082
|
+
blocksCount: 0,
|
|
1083
|
+
visibilitySet: VISIBILITY_SET_ALL_TRUE
|
|
1081
1084
|
}
|
|
1082
1085
|
}
|
|
1083
1086
|
|
|
@@ -1465,16 +1468,22 @@ function processColumnTick() {
|
|
|
1465
1468
|
const banners: Record<string, BannerMeta> = {}
|
|
1466
1469
|
const beTarget = { signs, heads, banners }
|
|
1467
1470
|
const beOpts = { disableBlockEntityTextures: world.config.disableBlockEntityTextures }
|
|
1471
|
+
const { occludingLookup } = getBlockMeta(version)
|
|
1472
|
+
const visGraph = new VisGraph()
|
|
1468
1473
|
const cursor = new Vec3(0, 0, 0)
|
|
1469
1474
|
for (cursor.y = sy; cursor.y < sy + sectionHeight; cursor.y++) {
|
|
1470
1475
|
for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
|
|
1471
1476
|
for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
|
|
1472
1477
|
const b = world.getBlock(cursor)
|
|
1473
1478
|
if (!b) continue
|
|
1479
|
+
if (occludingLookup[b.stateId]) {
|
|
1480
|
+
visGraph.setOpaque(cursor.x - sx, cursor.y - sy, cursor.z - sz)
|
|
1481
|
+
}
|
|
1474
1482
|
collectBlockEntityMetadata(b, cursor.x, cursor.y, cursor.z, beTarget, beOpts, world)
|
|
1475
1483
|
}
|
|
1476
1484
|
}
|
|
1477
1485
|
}
|
|
1486
|
+
const sectionVisibilitySet = packVisibilitySet(visGraph.resolve())
|
|
1478
1487
|
|
|
1479
1488
|
let geometry: MesherGeometryOutput
|
|
1480
1489
|
let transferable: any[] = []
|
|
@@ -1516,7 +1525,8 @@ function processColumnTick() {
|
|
|
1516
1525
|
// `B:` debug overlay stat) and matches the per-section path's
|
|
1517
1526
|
// semantics: number of blocks that contributed faces to this
|
|
1518
1527
|
// section's geometry.
|
|
1519
|
-
blocksCount: sectionBlocksCount
|
|
1528
|
+
blocksCount: sectionBlocksCount,
|
|
1529
|
+
visibilitySet: sectionVisibilitySet
|
|
1520
1530
|
}
|
|
1521
1531
|
if (exported.shaderCubes) {
|
|
1522
1532
|
geometry.shaderCubes = {
|
|
@@ -1573,6 +1583,7 @@ function processColumnTick() {
|
|
|
1573
1583
|
}
|
|
1574
1584
|
} else {
|
|
1575
1585
|
geometry = makeEmptyColumnGeometry(sx, sy, sz, sectionHeight, false)
|
|
1586
|
+
geometry.visibilitySet = sectionVisibilitySet
|
|
1576
1587
|
// Still attach block entity metadata so the main thread sees
|
|
1577
1588
|
// signs/heads/banners even for empty-mesh sections.
|
|
1578
1589
|
geometry.signs = signs
|