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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.91",
3
+ "version": "0.1.92",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -21,6 +21,9 @@ export const defaultWorldRendererConfig = {
21
21
  debugModelVariant: undefined as undefined | number[],
22
22
  futuristicReveal: false,
23
23
 
24
+ /** Master toggle for section occlusion graph (smart / cave cull). Disabled in spectator. */
25
+ smartCull: true,
26
+
24
27
  // Performance settings
25
28
  wasmMesher: true,
26
29
  /** Render full 1×1 cubes through the instanced shader path (requires WebGL2). */
@@ -54,7 +54,10 @@ export abstract class WorldRendererCommon<WorkerSend = any, WorkerReceive = any>
54
54
  chunksRenderBelowEnabled: false,
55
55
  chunksRenderDistanceOverride: undefined as number | undefined,
56
56
  chunksRenderDistanceEnabled: false,
57
- disableEntities: false
57
+ disableEntities: false,
58
+ /** Tint section borders by occlusion BFS step (perf debug overlay). */
59
+ caveCullingDebug: false,
60
+ smartCull: true
58
61
  // disableParticles: false
59
62
  })
60
63
 
@@ -27,6 +27,7 @@ export interface ExportedSection {
27
27
  count: number
28
28
  formatVersion: 3
29
29
  }
30
+ visibilitySet?: number
30
31
  }
31
32
 
32
33
  export interface ExportedWorldGeometry {
@@ -11,6 +11,8 @@ import { MesherGeometryOutput, HighestBlockInfo } from './shared'
11
11
  import { collectBlockEntityMetadata } from './blockEntityMetadata'
12
12
  import { preflatBlockCalculation, resolveBlockPropertiesForMeshing } from './blockPropertiesForMeshing'
13
13
  import { faceIsCulled, roundCardinalDir } from './faceOcclusion'
14
+ import { getOccludingBlockMeta } from './occludingBlocks'
15
+ import { packVisibilitySet, VisGraph } from './visGraph'
14
16
 
15
17
  export { preflatBlockCalculation, resolveBlockPropertiesForMeshing } from './blockPropertiesForMeshing'
16
18
 
@@ -540,12 +542,18 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
540
542
  indicesCount: 0
541
543
  }
542
544
 
545
+ const { occludingLookup } = getOccludingBlockMeta(world.config.version)
546
+ const visGraph = new VisGraph()
547
+
543
548
  const cursor = new Vec3(0, 0, 0)
544
549
  for (cursor.y = sy; cursor.y < sy + readHeight; cursor.y++) {
545
550
  for (cursor.z = sz; cursor.z < sz + 16; cursor.z++) {
546
551
  for (cursor.x = sx; cursor.x < sx + 16; cursor.x++) {
547
552
  let block = world.getBlock(cursor, blockProvider, attr)!
548
553
  if (INVISIBLE_BLOCKS.has(block.name)) continue
554
+ if (occludingLookup[block.stateId]) {
555
+ visGraph.setOpaque(cursor.x - sx, cursor.y - sy, cursor.z - sz)
556
+ }
549
557
  collectBlockEntityMetadata(block, cursor.x, cursor.y, cursor.z, attr, { disableBlockEntityTextures: world.config.disableBlockEntityTextures }, world)
550
558
  const biome = block.biome.name
551
559
 
@@ -680,6 +688,8 @@ export function getSectionGeometry(sx: number, sy: number, sz: number, world: Wo
680
688
  delete attr.uvs
681
689
  }
682
690
 
691
+ attr.visibilitySet = packVisibilitySet(visGraph.resolve())
692
+
683
693
  return attr
684
694
  }
685
695
 
@@ -0,0 +1,53 @@
1
+ //@ts-nocheck
2
+ import MinecraftData from 'minecraft-data'
3
+ import PrismarineBlockLoader from 'prismarine-block'
4
+ import moreBlockDataGeneratedJson from '../lib/moreBlockDataGenerated.json'
5
+
6
+ export type OccludingBlockMeta = {
7
+ occludingBlocks: Uint16Array
8
+ occludingLookup: Uint8Array
9
+ }
10
+
11
+ const metaCache = new Map<string, OccludingBlockMeta>()
12
+
13
+ const isCube = (shapes: unknown) => {
14
+ if (!shapes || !Array.isArray(shapes) || shapes.length !== 1) return false
15
+ const s = shapes[0] as number[]
16
+ return s[0] === 0 && s[1] === 0 && s[2] === 0 && s[3] === 1 && s[4] === 1 && s[5] === 1
17
+ }
18
+
19
+ /** Opaque full-cube state ids used for face culling and VisGraph — single source of truth. */
20
+ export const getOccludingBlockMeta = (version: string): OccludingBlockMeta => {
21
+ const cached = metaCache.get(version)
22
+ if (cached) return cached
23
+
24
+ const mcData = MinecraftData(version)
25
+ const Block = PrismarineBlockLoader(version)
26
+ const noOcclusionsSet = new Set(Object.keys(moreBlockDataGeneratedJson.noOcclusions))
27
+
28
+ const occludingBlockIds: number[] = []
29
+ for (const idStr of Object.keys((mcData as { blocksByStateId: Record<string, unknown> }).blocksByStateId)) {
30
+ const id = Number(idStr)
31
+ if (!id) continue
32
+ const b = (
33
+ Block as { fromStateId: (id: number, y: number) => { transparent?: boolean; boundingBox?: string; name: string; shapes?: unknown } | null }
34
+ ).fromStateId(id, 0)
35
+ if (!b) continue
36
+ if (b.transparent) continue
37
+ if (b.boundingBox !== 'block') continue
38
+ if (noOcclusionsSet.has(b.name)) continue
39
+ if (!isCube(b.shapes)) continue
40
+ occludingBlockIds.push(id)
41
+ }
42
+
43
+ const occludingBlocks = new Uint16Array(occludingBlockIds)
44
+ const maxId = occludingBlockIds.length ? Math.max(...occludingBlockIds) : 0
45
+ const occludingLookup = new Uint8Array(maxId + 1)
46
+ for (const id of occludingBlockIds) {
47
+ occludingLookup[id] = 1
48
+ }
49
+
50
+ const meta = { occludingBlocks, occludingLookup }
51
+ metaCache.set(version, meta)
52
+ return meta
53
+ }
@@ -82,6 +82,8 @@ export type MesherGeometryOutput = {
82
82
  count: number
83
83
  formatVersion: 3
84
84
  }
85
+ /** Packed VisibilitySet (36 bits) for section occlusion / smart cull. */
86
+ visibilitySet?: number
85
87
  }
86
88
 
87
89
  export interface MesherMainEvents {
@@ -0,0 +1,62 @@
1
+ //@ts-nocheck
2
+ import { test, expect } from 'vitest'
3
+ import { Direction, VisibilitySet, packVisibilitySet, unpackVisibilitySet, visibilityBetweenPacked } from '../visibilitySet'
4
+ import { VisGraph, computeSectionVisibilitySet } from '../visGraph'
5
+
6
+ test('empty section → all face pairs visible', () => {
7
+ const bits = computeSectionVisibilitySet(16, () => false)
8
+ const vs = unpackVisibilitySet(bits)
9
+ for (let a = 0; a < 6; a++) {
10
+ for (let b = 0; b < 6; b++) {
11
+ if (a === b) continue
12
+ expect(vs.visibilityBetween(a, b)).toBe(true)
13
+ }
14
+ }
15
+ })
16
+
17
+ test('solid stone cube → no face pairs', () => {
18
+ const bits = computeSectionVisibilitySet(16, () => true)
19
+ const vs = unpackVisibilitySet(bits)
20
+ for (let a = 0; a < 6; a++) {
21
+ for (let b = 0; b < 6; b++) {
22
+ if (a === b) continue
23
+ expect(vs.visibilityBetween(a, b)).toBe(false)
24
+ }
25
+ }
26
+ })
27
+
28
+ test('tunnel through section N↔S only → only N/S connected', () => {
29
+ const bits = computeSectionVisibilitySet(16, (lx, _ly, lz) => {
30
+ // walls on east/west, open north-south corridor along z
31
+ return lx === 0 || lx === 15
32
+ })
33
+ const vs = unpackVisibilitySet(bits)
34
+ expect(vs.visibilityBetween(Direction.NORTH, Direction.SOUTH)).toBe(true)
35
+ expect(vs.visibilityBetween(Direction.EAST, Direction.WEST)).toBe(false)
36
+ expect(vs.visibilityBetween(Direction.NORTH, Direction.EAST)).toBe(false)
37
+ })
38
+
39
+ test('VisibilitySet pack/unpack roundtrip', () => {
40
+ const vs = new VisibilitySet()
41
+ vs.add(new Set([Direction.NORTH, Direction.SOUTH]))
42
+ const packed = packVisibilitySet(vs)
43
+ const restored = unpackVisibilitySet(packed)
44
+ expect(restored.visibilityBetween(Direction.NORTH, Direction.SOUTH)).toBe(true)
45
+ expect(restored.visibilityBetween(Direction.EAST, Direction.WEST)).toBe(false)
46
+ expect(visibilityBetweenPacked(packed, Direction.NORTH, Direction.SOUTH)).toBe(true)
47
+ })
48
+
49
+ test('VisGraph fast path: mostly opaque → all visible', () => {
50
+ const graph = new VisGraph()
51
+ let count = 0
52
+ for (let ly = 0; ly < 16 && count < 3841; ly++) {
53
+ for (let lz = 0; lz < 16 && count < 3841; lz++) {
54
+ for (let lx = 0; lx < 16 && count < 3841; lx++) {
55
+ graph.setOpaque(lx, ly, lz)
56
+ count++
57
+ }
58
+ }
59
+ }
60
+ const vs = graph.resolve()
61
+ expect(vs.visibilityBetween(Direction.NORTH, Direction.SOUTH)).toBe(true)
62
+ })
@@ -0,0 +1,155 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Port of Minecraft VisGraph — flood-fill air pockets in a 16³ section.
4
+ * @see extracted_minecraft_data/client/net/minecraft/client/renderer/chunk/VisGraph.java
5
+ */
6
+
7
+ import { Direction, DIRECTIONS, VisibilitySet, packVisibilitySet } from './visibilitySet'
8
+
9
+ const LEN = 16
10
+ const SIZE = 4096
11
+ const DX = 1
12
+ const DZ = 16
13
+ const DY = 256
14
+ const INVALID_INDEX = -1
15
+
16
+ function getIndex(x: number, y: number, z: number): number {
17
+ return (x << 0) | (y << 8) | (z << 4)
18
+ }
19
+
20
+ const INDEX_OF_EDGES: number[] = (() => {
21
+ const edges: number[] = []
22
+ for (let x = 0; x < 16; x++) {
23
+ for (let z = 0; z < 16; z++) {
24
+ for (let y = 0; y < 16; y++) {
25
+ if (x === 0 || x === 15 || z === 0 || z === 15 || y === 0 || y === 15) {
26
+ edges.push(getIndex(x, y, z))
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return edges
32
+ })()
33
+
34
+ class BitSet4096 {
35
+ private readonly words = new Uint32Array(128)
36
+
37
+ get(index: number): boolean {
38
+ return ((this.words[index >> 5]! >> (index & 31)) & 1) === 1
39
+ }
40
+
41
+ set(index: number, value: boolean): void {
42
+ if (value) {
43
+ this.words[index >> 5]! |= 1 << (index & 31)
44
+ } else {
45
+ this.words[index >> 5]! &= ~(1 << (index & 31))
46
+ }
47
+ }
48
+ }
49
+
50
+ export class VisGraph {
51
+ private readonly bitSet = new BitSet4096()
52
+ private empty = SIZE
53
+
54
+ setOpaque(x: number, y: number, z: number): void {
55
+ const index = getIndex(x & 15, y & 15, z & 15)
56
+ if (!this.bitSet.get(index)) {
57
+ this.bitSet.set(index, true)
58
+ this.empty--
59
+ }
60
+ }
61
+
62
+ resolve(): VisibilitySet {
63
+ const result = new VisibilitySet()
64
+ if (SIZE - this.empty < 256) {
65
+ result.setAll(true)
66
+ } else if (this.empty === 0) {
67
+ result.setAll(false)
68
+ } else {
69
+ for (const edgeIndex of INDEX_OF_EDGES) {
70
+ if (!this.bitSet.get(edgeIndex)) {
71
+ result.add(this.floodFill(edgeIndex))
72
+ }
73
+ }
74
+ }
75
+ return result
76
+ }
77
+
78
+ private floodFill(startIndex: number): Set<Direction> {
79
+ const faces = new Set<Direction>()
80
+ const queue: number[] = [startIndex]
81
+ let head = 0
82
+ this.bitSet.set(startIndex, true)
83
+
84
+ while (head < queue.length) {
85
+ const index = queue[head++]!
86
+ this.addEdges(index, faces)
87
+
88
+ for (const dir of DIRECTIONS) {
89
+ const neighbor = this.getNeighborIndexAtFace(index, dir)
90
+ if (neighbor >= 0 && !this.bitSet.get(neighbor)) {
91
+ this.bitSet.set(neighbor, true)
92
+ queue.push(neighbor)
93
+ }
94
+ }
95
+ }
96
+
97
+ return faces
98
+ }
99
+
100
+ private addEdges(index: number, faces: Set<Direction>): void {
101
+ const x = (index >> 0) & 15
102
+ if (x === 0) faces.add(Direction.WEST)
103
+ else if (x === 15) faces.add(Direction.EAST)
104
+
105
+ const y = (index >> 8) & 15
106
+ if (y === 0) faces.add(Direction.DOWN)
107
+ else if (y === 15) faces.add(Direction.UP)
108
+
109
+ const z = (index >> 4) & 15
110
+ if (z === 0) faces.add(Direction.NORTH)
111
+ else if (z === 15) faces.add(Direction.SOUTH)
112
+ }
113
+
114
+ private getNeighborIndexAtFace(index: number, dir: Direction): number {
115
+ switch (dir) {
116
+ case Direction.DOWN:
117
+ if (((index >> 8) & 15) === 0) return INVALID_INDEX
118
+ return index - DY
119
+ case Direction.UP:
120
+ if (((index >> 8) & 15) === 15) return INVALID_INDEX
121
+ return index + DY
122
+ case Direction.NORTH:
123
+ if (((index >> 4) & 15) === 0) return INVALID_INDEX
124
+ return index - DZ
125
+ case Direction.SOUTH:
126
+ if (((index >> 4) & 15) === 15) return INVALID_INDEX
127
+ return index + DZ
128
+ case Direction.WEST:
129
+ if (((index >> 0) & 15) === 0) return INVALID_INDEX
130
+ return index - DX
131
+ case Direction.EAST:
132
+ if (((index >> 0) & 15) === 15) return INVALID_INDEX
133
+ return index + DX
134
+ default:
135
+ return INVALID_INDEX
136
+ }
137
+ }
138
+ }
139
+
140
+ export function computeSectionVisibilitySet(sectionHeight: number, isOpaqueAt: (lx: number, ly: number, lz: number) => boolean): number {
141
+ const graph = new VisGraph()
142
+ const height = Math.min(sectionHeight, LEN)
143
+ for (let ly = 0; ly < height; ly++) {
144
+ for (let lz = 0; lz < LEN; lz++) {
145
+ for (let lx = 0; lx < LEN; lx++) {
146
+ if (isOpaqueAt(lx, ly, lz)) {
147
+ graph.setOpaque(lx, ly, lz)
148
+ }
149
+ }
150
+ }
151
+ }
152
+ return packVisibilitySet(graph.resolve())
153
+ }
154
+
155
+ export { packVisibilitySet } from './visibilitySet'
@@ -0,0 +1,124 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Port of Minecraft VisibilitySet — 6×6 face connectivity matrix for section air pockets.
4
+ * @see extracted_minecraft_data/client/net/minecraft/client/renderer/chunk/VisibilitySet.java
5
+ */
6
+
7
+ /** Java Direction.values() order: DOWN, UP, NORTH, SOUTH, WEST, EAST */
8
+ export enum Direction {
9
+ DOWN = 0,
10
+ UP = 1,
11
+ NORTH = 2,
12
+ SOUTH = 3,
13
+ WEST = 4,
14
+ EAST = 5
15
+ }
16
+
17
+ export const DIRECTIONS: readonly Direction[] = [Direction.DOWN, Direction.UP, Direction.NORTH, Direction.SOUTH, Direction.WEST, Direction.EAST]
18
+
19
+ const FACINGS = DIRECTIONS.length
20
+
21
+ /** All 36 bits set — fail-open default when visibility data is missing. */
22
+ export const VISIBILITY_SET_ALL_TRUE = 2 ** 36 - 1
23
+
24
+ export class VisibilitySet {
25
+ private data = new Uint8Array(Math.ceil((FACINGS * FACINGS) / 8))
26
+
27
+ add(faces: Set<Direction>): void {
28
+ for (const a of faces) {
29
+ for (const b of faces) {
30
+ this.set(a, b, true)
31
+ }
32
+ }
33
+ }
34
+
35
+ set(a: Direction, b: Direction, value: boolean): void {
36
+ this.setBit(a + b * FACINGS, value)
37
+ this.setBit(b + a * FACINGS, value)
38
+ }
39
+
40
+ setAll(value: boolean): void {
41
+ this.data.fill(value ? 0xff : 0)
42
+ }
43
+
44
+ visibilityBetween(a: Direction, b: Direction): boolean {
45
+ return this.getBit(a + b * FACINGS)
46
+ }
47
+
48
+ static allTrue(): VisibilitySet {
49
+ const vs = new VisibilitySet()
50
+ vs.setAll(true)
51
+ return vs
52
+ }
53
+
54
+ static allFalse(): VisibilitySet {
55
+ const vs = new VisibilitySet()
56
+ vs.setAll(false)
57
+ return vs
58
+ }
59
+
60
+ private setBit(index: number, value: boolean): void {
61
+ const byteIndex = index >> 3
62
+ const bitIndex = index & 7
63
+ if (value) {
64
+ this.data[byteIndex]! |= 1 << bitIndex
65
+ } else {
66
+ this.data[byteIndex]! &= ~(1 << bitIndex)
67
+ }
68
+ }
69
+
70
+ private getBit(index: number): boolean {
71
+ const byteIndex = index >> 3
72
+ const bitIndex = index & 7
73
+ return ((this.data[byteIndex]! >> bitIndex) & 1) === 1
74
+ }
75
+ }
76
+
77
+ export function packVisibilitySet(vs: VisibilitySet): number {
78
+ let bits = 0
79
+ for (let a = 0; a < FACINGS; a++) {
80
+ for (let b = 0; b < FACINGS; b++) {
81
+ if (vs.visibilityBetween(a, b)) {
82
+ bits += 2 ** (a + b * FACINGS)
83
+ }
84
+ }
85
+ }
86
+ return bits
87
+ }
88
+
89
+ export function unpackVisibilitySet(bits: number): VisibilitySet {
90
+ const vs = new VisibilitySet()
91
+ for (let a = 0; a < FACINGS; a++) {
92
+ for (let b = 0; b < FACINGS; b++) {
93
+ const idx = a + b * FACINGS
94
+ if (Math.floor(bits / 2 ** idx) % 2 === 1) {
95
+ vs.set(a, b, true)
96
+ }
97
+ }
98
+ }
99
+ return vs
100
+ }
101
+
102
+ export function visibilityBetweenPacked(bits: number, a: Direction, b: Direction): boolean {
103
+ const idx = a + b * FACINGS
104
+ return Math.floor(bits / 2 ** idx) % 2 === 1
105
+ }
106
+
107
+ export function oppositeDirection(dir: Direction): Direction {
108
+ switch (dir) {
109
+ case Direction.DOWN:
110
+ return Direction.UP
111
+ case Direction.UP:
112
+ return Direction.DOWN
113
+ case Direction.NORTH:
114
+ return Direction.SOUTH
115
+ case Direction.SOUTH:
116
+ return Direction.NORTH
117
+ case Direction.WEST:
118
+ return Direction.EAST
119
+ case Direction.EAST:
120
+ return Direction.WEST
121
+ default:
122
+ return dir
123
+ }
124
+ }