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.
@@ -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 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