minecraft-renderer 0.1.91 → 0.1.93

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.
@@ -243,13 +243,13 @@ test('ChunkMeshManager: hidden section excluded from draw spans', () => {
243
243
  camera.lookAt(8, 8, 8)
244
244
  camera.updateMatrixWorld()
245
245
 
246
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
246
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
247
247
  expect(manager.globalLegacyBlendBuffer?.getVisibleIndexSpans().length).toBe(0)
248
248
 
249
249
  section.visible = true
250
250
  const blendBuf = manager.globalLegacyBlendBuffer!
251
251
  while (blendBuf.hasPendingUploads()) blendBuf.uploadDirtyRange()
252
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
252
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
253
253
  expect(manager.globalLegacyBlendBuffer?.getVisibleIndexSpans().length).toBeGreaterThan(0)
254
254
 
255
255
  manager.cleanupSection(key)
@@ -323,15 +323,15 @@ test('ChunkMeshManager: hidden cube section excluded from draw spans', () => {
323
323
  drainCubeUploads(manager)
324
324
 
325
325
  const camera = makeCullCamera()
326
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
326
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
327
327
  expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBeGreaterThan(0)
328
328
 
329
329
  section.visible = false
330
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
330
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
331
331
  expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBe(0)
332
332
 
333
333
  section.visible = true
334
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
334
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
335
335
  expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBeGreaterThan(0)
336
336
 
337
337
  manager.cleanupSection(key)
@@ -346,7 +346,7 @@ test('ChunkMeshManager: finishChunkDisplay reveals cube spans and marks cull dir
346
346
  expect(manager.globalBlockBuffer?.hasSection(key)).toBe(true)
347
347
 
348
348
  const camera = makeCullCamera()
349
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
349
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
350
350
  expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBe(0)
351
351
 
352
352
  const markCullDirtySpy = vi.spyOn(manager, 'markCullDirty')
@@ -355,7 +355,7 @@ test('ChunkMeshManager: finishChunkDisplay reveals cube spans and marks cull dir
355
355
  expect(markCullDirtySpy).toHaveBeenCalled()
356
356
 
357
357
  drainCubeUploads(manager)
358
- manager.updateSectionCullAndSort(camera, 8, 8, 20)
358
+ manager.updateSectionCullAndSort(camera, 8, 8, 20, false)
359
359
  expect(manager.globalBlockBuffer?.getVisibleSpans().length).toBeGreaterThan(0)
360
360
 
361
361
  markCullDirtySpy.mockRestore()
@@ -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
@@ -565,6 +584,9 @@ export class WorldRendererThree extends WorldRendererCommon {
565
584
  this.onReactiveConfigUpdated('futuristicReveal', () => {
566
585
  this.updateModulesFromConfig()
567
586
  })
587
+ this.onReactiveDebugUpdated('disableEntities', () => {
588
+ this.entities.syncSceneAttachment()
589
+ })
568
590
 
569
591
  let currentHandRenderer = this.displayOptions.inWorldRenderingConfig.handRenderer
570
592
  this.onReactiveConfigUpdated('handRenderer', value => {
@@ -787,6 +809,8 @@ export class WorldRendererThree extends WorldRendererCommon {
787
809
  const s = gb.legacyBlend
788
810
  text += `LEG-B: ${formatCompact(s.used)}/${formatCompact(s.capacity)}q `
789
811
  }
812
+ const drawn = this.chunkMeshManager.getDrawnStats()
813
+ text += `DRAWN: ${formatCompact(drawn.cubeFaces)}f ${formatCompact(drawn.legacyOpaqueQuads)}q ${formatCompact(drawn.legacyBlendQuads)}q `
790
814
  text += `MEM: ${this.chunkMeshManager.getEstimatedMemoryUsage().total} `
791
815
  const camCollisionBytes = this.cameraCollisionBlockCache.getAllocatedBytes()
792
816
  if (camCollisionBytes > 0) {
@@ -1285,11 +1309,6 @@ export class WorldRendererThree extends WorldRendererCommon {
1285
1309
  }
1286
1310
 
1287
1311
  let entitiesRenderMs = 0
1288
- if (!this.reactiveDebugParams.disableEntities) {
1289
- const entitiesStart = performance.now()
1290
- this.entities.render()
1291
- entitiesRenderMs = performance.now() - entitiesStart
1292
- }
1293
1312
 
1294
1313
  // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style
1295
1314
  const cam =
@@ -1340,10 +1359,16 @@ export class WorldRendererThree extends WorldRendererCommon {
1340
1359
  this.chunkMeshManager.markCullDirtyIfBufferStateChanged()
1341
1360
  this.chunkMeshManager.setLegacyCameraOrigin(camX, camY, camZ)
1342
1361
  this.chunkMeshManager.updateCullDirtyFromCamera(cam, camX, camY, camZ)
1362
+ const smartCull = this.isSmartCullEnabled()
1363
+ this.chunkMeshManager.notifySmartCullChanged(smartCull)
1343
1364
  if (this.chunkMeshManager.cullDirty) {
1344
- this.chunkMeshManager.updateSectionCullAndSort(cam, this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
1365
+ this.chunkMeshManager.updateSectionCullAndSort(cam, this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z, smartCull)
1345
1366
  this.chunkMeshManager.clearCullDirty()
1346
1367
  }
1368
+ this.chunkMeshManager.updateCaveCullingDebug(this.reactiveDebugParams.caveCullingDebug, smartCull)
1369
+ const entitiesStart = performance.now()
1370
+ this.entities.render()
1371
+ entitiesRenderMs = performance.now() - entitiesStart
1347
1372
  this.chunkMeshManager.sortVisibleBlendSections(camX, camY, camZ)
1348
1373
  if (!didBlendAllAttrUpload && globalLegacyBlendBuffer?.hasPendingIndexUploads()) {
1349
1374
  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 Block = PrismarineBlockLoader(version)
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