minecraft-renderer 0.1.72 → 0.1.74

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.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/mesher.js +81 -81
  3. package/dist/mesher.js.map +3 -3
  4. package/dist/mesherWasm.js +1183 -943
  5. package/dist/minecraft-renderer.js +253 -80
  6. package/dist/minecraft-renderer.js.meta.json +1 -1
  7. package/dist/threeWorker.js +1735 -1002
  8. package/package.json +3 -3
  9. package/src/graphicsBackend/config.ts +4 -0
  10. package/src/graphicsBackend/rendererDefaultOptions.ts +2 -7
  11. package/src/graphicsBackend/rendererOptionsSync.ts +1 -1
  12. package/src/graphicsBackend/types.ts +1 -0
  13. package/src/lib/bakeLegacyLight.ts +17 -0
  14. package/src/lib/bindAbortableListener.ts +1 -1
  15. package/src/lib/blockEntityLightRegistry.test.ts +18 -0
  16. package/src/lib/blockEntityLightRegistry.ts +75 -0
  17. package/src/lib/blockEntityLighting.test.ts +30 -0
  18. package/src/lib/blockEntityLighting.ts +53 -0
  19. package/src/lib/createPlayerObject.ts +1 -1
  20. package/src/lib/worldrendererCommon.reconfigure.test.ts +4 -1
  21. package/src/lib/worldrendererCommon.removeColumn.test.ts +8 -4
  22. package/src/lib/worldrendererCommon.ts +15 -7
  23. package/src/mesher-shared/blockEntityMetadata.test.ts +33 -0
  24. package/src/mesher-shared/blockEntityMetadata.ts +19 -3
  25. package/src/mesher-shared/exportedGeometryTypes.ts +11 -0
  26. package/src/mesher-shared/models.ts +161 -92
  27. package/src/mesher-shared/shared.ts +15 -4
  28. package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +40 -0
  29. package/src/mesher-shared/world.ts +12 -0
  30. package/src/mesher-shared/worldLighting.test.ts +54 -0
  31. package/src/playground/baseScene.ts +1 -1
  32. package/src/three/bannerRenderer.ts +14 -4
  33. package/src/three/chunkMeshManager.ts +663 -69
  34. package/src/three/cubeDrawSpans.ts +74 -0
  35. package/src/three/cubeMultiDraw.ts +119 -0
  36. package/src/three/documentRenderer.ts +0 -2
  37. package/src/three/entities.ts +7 -7
  38. package/src/three/entity/EntityMesh.ts +7 -5
  39. package/src/three/entity/gltfAnimationUtils.ts +5 -3
  40. package/src/three/globalBlockBuffer.ts +208 -12
  41. package/src/three/globalLegacyBuffer.ts +701 -0
  42. package/src/three/graphicsBackendBase.ts +9 -5
  43. package/src/three/itemMesh.ts +6 -3
  44. package/src/three/legacySectionCull.ts +85 -0
  45. package/src/three/modules/rain.ts +22 -21
  46. package/src/three/modules/sciFiWorldReveal.ts +347 -703
  47. package/src/three/modules/starfield.ts +19 -6
  48. package/src/three/sectionRaycastAabb.ts +25 -0
  49. package/src/three/shaders/cubeBlockShader.ts +80 -17
  50. package/src/three/shaders/legacyBlockShader.ts +292 -0
  51. package/src/three/skyboxRenderer.ts +1 -1
  52. package/src/three/tests/chunkMeshManagerLegacy.test.ts +286 -0
  53. package/src/three/tests/cubeDrawSpans.test.ts +73 -0
  54. package/src/three/tests/globalLegacyBuffer.test.ts +360 -0
  55. package/src/three/tests/legacySectionCull.test.ts +80 -0
  56. package/src/three/tests/signTextureCache.test.ts +83 -0
  57. package/src/three/threeJsMedia.ts +2 -2
  58. package/src/three/waypointSprite.ts +2 -2
  59. package/src/three/world/cursorBlock.ts +1 -0
  60. package/src/three/world/vr.ts +2 -2
  61. package/src/three/worldGeometryExport.ts +83 -26
  62. package/src/three/worldRendererThree.ts +100 -30
  63. package/src/wasm-mesher/bridge/render-from-wasm.ts +214 -72
  64. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +18 -6
  65. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  66. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +20 -0
  67. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +80 -12
  68. package/src/wasm-mesher/worker/mesherWasm.ts +70 -14
  69. package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +11 -0
  70. package/src/wasm-mesher/worker/mesherWasmLightDirty.ts +15 -0
@@ -0,0 +1,286 @@
1
+ //@ts-nocheck
2
+ import { test, expect, vi } from 'vitest'
3
+ import * as THREE from 'three'
4
+
5
+ vi.mock('../entity/EntityMesh', () => ({
6
+ getMesh: vi.fn(),
7
+ }))
8
+
9
+ import { ChunkMeshManager } from '../chunkMeshManager'
10
+ import type { WorldRendererThree } from '../worldRendererThree'
11
+ import type { MesherGeometryOutput } from '../../mesher-shared/shared'
12
+
13
+ function makeQuadArrays () {
14
+ const positions = new Float32Array([
15
+ -1, -1, -1,
16
+ -1, 1, -1,
17
+ -1, 1, 1,
18
+ -1, -1, 1,
19
+ ])
20
+ const colors = new Float32Array(12).fill(1)
21
+ const skyLights = new Float32Array(4).fill(1)
22
+ const blockLights = new Float32Array(4).fill(0)
23
+ const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1])
24
+ const indices = new Uint32Array([0, 1, 2, 0, 2, 3])
25
+ return { positions, colors, skyLights, blockLights, uvs, indices }
26
+ }
27
+
28
+ function makeBlendOnlyGeometry (): MesherGeometryOutput {
29
+ const blend = makeQuadArrays()
30
+ return {
31
+ sectionYNumber: 0,
32
+ chunkKey: '0,0',
33
+ sectionStartY: 0,
34
+ sectionEndY: 16,
35
+ sectionStartX: 0,
36
+ sectionEndX: 16,
37
+ sectionStartZ: 0,
38
+ sectionEndZ: 16,
39
+ sx: 8,
40
+ sy: 8,
41
+ sz: 8,
42
+ positions: new Float32Array(0),
43
+ normals: new Float32Array(0),
44
+ colors: new Float32Array(0),
45
+ skyLights: new Float32Array(0),
46
+ blockLights: new Float32Array(0),
47
+ uvs: new Float32Array(0),
48
+ indices: new Uint32Array(0),
49
+ indicesCount: 0,
50
+ using32Array: true,
51
+ tiles: {},
52
+ heads: {},
53
+ signs: {},
54
+ banners: {},
55
+ hadErrors: false,
56
+ blocksCount: 1,
57
+ blend: {
58
+ positions: blend.positions,
59
+ normals: new Float32Array(12),
60
+ colors: blend.colors,
61
+ skyLights: blend.skyLights,
62
+ blockLights: blend.blockLights,
63
+ uvs: blend.uvs,
64
+ indices: blend.indices,
65
+ },
66
+ }
67
+ }
68
+
69
+ function makeMixedGeometry (): MesherGeometryOutput {
70
+ const opaque = makeQuadArrays()
71
+ const blend = makeQuadArrays()
72
+ return {
73
+ sectionYNumber: 0,
74
+ chunkKey: '0,0',
75
+ sectionStartY: 0,
76
+ sectionEndY: 16,
77
+ sectionStartX: 0,
78
+ sectionEndX: 16,
79
+ sectionStartZ: 0,
80
+ sectionEndZ: 16,
81
+ sx: 8,
82
+ sy: 8,
83
+ sz: 8,
84
+ positions: opaque.positions,
85
+ normals: new Float32Array(12),
86
+ colors: opaque.colors,
87
+ skyLights: opaque.skyLights,
88
+ blockLights: opaque.blockLights,
89
+ uvs: opaque.uvs,
90
+ indices: opaque.indices,
91
+ indicesCount: 6,
92
+ using32Array: true,
93
+ tiles: {},
94
+ heads: {},
95
+ signs: {},
96
+ banners: {},
97
+ hadErrors: false,
98
+ blocksCount: 2,
99
+ blend: {
100
+ positions: blend.positions,
101
+ normals: new Float32Array(12),
102
+ colors: blend.colors,
103
+ skyLights: blend.skyLights,
104
+ blockLights: blend.blockLights,
105
+ uvs: blend.uvs,
106
+ indices: blend.indices,
107
+ },
108
+ }
109
+ }
110
+
111
+ function makeInvalidBlendGeometry (): MesherGeometryOutput {
112
+ const geo = makeBlendOnlyGeometry()
113
+ const blend = geo.blend!
114
+ return {
115
+ ...geo,
116
+ blend: {
117
+ ...blend,
118
+ positions: new Float32Array([0, 0, 0, 1, 0, 0, 2, 0, 0]),
119
+ indices: new Uint32Array([0, 1, 2, 0, 2, 1, 3]),
120
+ },
121
+ }
122
+ }
123
+
124
+ type ManagerOptions = {
125
+ revealDefer?: boolean
126
+ }
127
+
128
+ function createManager (opts: ManagerOptions = {}): ChunkMeshManager {
129
+ const scene = new THREE.Scene()
130
+ const material = new THREE.MeshBasicMaterial()
131
+ const revealModule = opts.revealDefer
132
+ ? {
133
+ shouldDeferSectionGeometry: () => true,
134
+ }
135
+ : undefined
136
+ const worldRenderer = {
137
+ shaderCubeBlocksEnabled: () => false,
138
+ getModule: (name: string) => (name === 'futuristicReveal' ? revealModule : undefined),
139
+ sceneOrigin: {
140
+ track: () => {},
141
+ removeAndUntrack: () => {},
142
+ removeAndUntrackAll: () => {},
143
+ },
144
+ blockEntities: {},
145
+ worldRendererConfig: {},
146
+ } as unknown as WorldRendererThree
147
+ return new ChunkMeshManager(worldRenderer, scene, material, 256, 1)
148
+ }
149
+
150
+ test('ChunkMeshManager: blend section routes to global blend buffer', () => {
151
+ const manager = createManager()
152
+ const key = '0,0,0'
153
+ const geo = makeBlendOnlyGeometry()
154
+
155
+ manager.updateSection(key, geo)
156
+
157
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(true)
158
+ expect(manager.sectionObjects[key]?.hasBlendMesh).toBe(false)
159
+ expect(manager.sectionUsesPooledLegacyMesh(key)).toBe(false)
160
+
161
+ manager.cleanupSection(key)
162
+ manager.dispose()
163
+ })
164
+
165
+ test('ChunkMeshManager: cleanup removes blend from global buffer', () => {
166
+ const manager = createManager()
167
+ const key = '0,0,0'
168
+ manager.updateSection(key, makeBlendOnlyGeometry())
169
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(true)
170
+
171
+ manager.cleanupSection(key)
172
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(false)
173
+
174
+ manager.dispose()
175
+ })
176
+
177
+ test('ChunkMeshManager: hidden section excluded from draw spans', () => {
178
+ const manager = createManager()
179
+ const key = '0,0,0'
180
+ manager.updateSection(key, makeBlendOnlyGeometry())
181
+ const section = manager.sectionObjects[key]!
182
+ section.visible = false
183
+
184
+ const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 1000)
185
+ camera.position.set(8, 8, 20)
186
+ camera.lookAt(8, 8, 8)
187
+ camera.updateMatrixWorld()
188
+
189
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
190
+ expect(manager.globalLegacyBlendBuffer?.mesh.geometry.groups.length).toBe(0)
191
+
192
+ section.visible = true
193
+ manager.updateSectionCullAndSort(camera, 8, 8, 20)
194
+ expect(manager.globalLegacyBlendBuffer?.mesh.geometry.groups.length).toBeGreaterThan(0)
195
+
196
+ manager.cleanupSection(key)
197
+ manager.dispose()
198
+ })
199
+
200
+ test('ChunkMeshManager: reveal defer blend migrates to global and releases pool', () => {
201
+ const manager = createManager({ revealDefer: true })
202
+ const key = '0,0,0'
203
+ const geo = makeBlendOnlyGeometry()
204
+
205
+ manager.updateSection(key, geo)
206
+
207
+ expect(manager.sectionObjects[key]?.hasBlendMesh).toBe(true)
208
+ expect(manager.sectionObjects[key]?.deferredLegacyBlend).toBeDefined()
209
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key) ?? false).toBe(false)
210
+ expect(manager.sectionUsesPooledLegacyMesh(key)).toBe(true)
211
+
212
+ manager.migrateDeferredLegacyToGlobal(key)
213
+
214
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(true)
215
+ expect(manager.sectionObjects[key]?.hasBlendMesh).toBe(false)
216
+ expect(manager.sectionObjects[key]?.deferredLegacyBlend).toBeUndefined()
217
+ expect(manager.sectionUsesPooledLegacyMesh(key)).toBe(false)
218
+
219
+ manager.cleanupSection(key)
220
+ manager.dispose()
221
+ })
222
+
223
+ test('ChunkMeshManager: invalid blend geometry falls back to pooled mesh', () => {
224
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
225
+ const manager = createManager()
226
+ const key = '0,0,0'
227
+
228
+ manager.updateSection(key, makeInvalidBlendGeometry())
229
+
230
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('blend invariant violation'))
231
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(false)
232
+ expect(manager.sectionObjects[key]?.hasBlendMesh).toBe(true)
233
+ expect(manager.sectionUsesPooledLegacyMesh(key)).toBe(true)
234
+
235
+ warn.mockRestore()
236
+ manager.cleanupSection(key)
237
+ manager.dispose()
238
+ })
239
+
240
+ test('ChunkMeshManager: raycastGlobalLegacySections rejects off-ray sections within center distance', () => {
241
+ const manager = createManager()
242
+ const onRayKey = '0,0,0'
243
+ const offRayKey = '0,2,0'
244
+
245
+ manager.updateSection(onRayKey, makeBlendOnlyGeometry())
246
+
247
+ const offRayGeo = makeBlendOnlyGeometry()
248
+ offRayGeo.sz = 40
249
+ manager.updateSection(offRayKey, offRayGeo)
250
+
251
+ expect(manager.globalLegacyBlendBuffer?.hasSection(onRayKey)).toBe(true)
252
+ expect(manager.globalLegacyBlendBuffer?.hasSection(offRayKey)).toBe(true)
253
+ expect(manager.sectionObjects[offRayKey]?.worldZ).toBe(40)
254
+
255
+ const origin = new THREE.Vector3(4, 8, 8)
256
+ const direction = new THREE.Vector3(1, 0, 0).normalize()
257
+ const raycaster = new THREE.Raycaster(origin, direction)
258
+ raycaster.far = 4
259
+
260
+ const hit = manager.raycastGlobalLegacySections(raycaster, origin, 80)
261
+ expect(hit).toBeDefined()
262
+ expect(hit!).toBeGreaterThan(2)
263
+ expect(hit!).toBeLessThan(4)
264
+
265
+ manager.cleanupSection(onRayKey)
266
+ manager.cleanupSection(offRayKey)
267
+ manager.dispose()
268
+ })
269
+
270
+ test('ChunkMeshManager: mixed opaque and blend route to separate global buffers', () => {
271
+ const manager = createManager()
272
+ const key = '0,0,0'
273
+
274
+ manager.updateSection(key, makeMixedGeometry())
275
+
276
+ expect(manager.globalLegacyBuffer?.hasSection(key)).toBe(true)
277
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(true)
278
+ expect(manager.sectionObjects[key]?.hasBlendMesh).toBe(false)
279
+ expect(manager.sectionUsesPooledLegacyMesh(key)).toBe(false)
280
+
281
+ manager.cleanupSection(key)
282
+ expect(manager.globalLegacyBuffer?.hasSection(key)).toBe(false)
283
+ expect(manager.globalLegacyBlendBuffer?.hasSection(key)).toBe(false)
284
+
285
+ manager.dispose()
286
+ })
@@ -0,0 +1,73 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, test } from 'vitest'
3
+ import {
4
+ buildVisibleCubeSpans,
5
+ MAX_CUBE_SPANS,
6
+ SPAN_GAP_TOLERANCE_FACES,
7
+ } from '../cubeDrawSpans'
8
+
9
+ describe('buildVisibleCubeSpans', () => {
10
+ test('contiguous slots merge into one span', () => {
11
+ const spans = buildVisibleCubeSpans([
12
+ { start: 0, count: 4 },
13
+ { start: 4, count: 2 },
14
+ ], 6)
15
+ expect(spans.length).toBe(1)
16
+ expect(spans[0]).toEqual({ start: 0, count: 6 })
17
+ })
18
+
19
+ test('scattered slots stay as multiple spans', () => {
20
+ const gap = SPAN_GAP_TOLERANCE_FACES + 1
21
+ const spans = buildVisibleCubeSpans([
22
+ { start: 0, count: 1 },
23
+ { start: 1 + gap, count: 1 },
24
+ ], 1 + gap + 1)
25
+ expect(spans.length).toBe(2)
26
+ expect(spans[0]).toEqual({ start: 0, count: 1 })
27
+ expect(spans[1]).toEqual({ start: 1 + gap, count: 1 })
28
+ })
29
+
30
+ test('full draw when most faces visible', () => {
31
+ const spans = buildVisibleCubeSpans([
32
+ { start: 0, count: 3 },
33
+ { start: 3, count: 3 },
34
+ { start: 6, count: 2 },
35
+ ], 8)
36
+ expect(spans.length).toBe(1)
37
+ expect(spans[0]).toEqual({ start: 0, count: 8 })
38
+ })
39
+
40
+ test('caps at MAX_CUBE_SPANS with full coverage', () => {
41
+ const visibleSectionCount = MAX_CUBE_SPANS + 5
42
+ const padFaces = SPAN_GAP_TOLERANCE_FACES + 1
43
+ const visibleSlots: Array<{ start: number, count: number }> = []
44
+ let cursor = 0
45
+
46
+ for (let i = 0; i < visibleSectionCount; i++) {
47
+ visibleSlots.push({ start: cursor, count: 1 })
48
+ cursor += 1
49
+ if (i < visibleSectionCount - 1) {
50
+ cursor += padFaces
51
+ }
52
+ }
53
+
54
+ const highWatermark = cursor
55
+ const spans = buildVisibleCubeSpans(visibleSlots, highWatermark)
56
+ expect(spans.length).toBe(MAX_CUBE_SPANS)
57
+
58
+ const covered = new Set<number>()
59
+ for (const span of spans) {
60
+ for (let f = span.start; f < span.start + span.count; f++) {
61
+ covered.add(f)
62
+ }
63
+ }
64
+ for (const slot of visibleSlots) {
65
+ expect(covered.has(slot.start)).toBe(true)
66
+ }
67
+ })
68
+
69
+ test('empty input returns empty spans', () => {
70
+ expect(buildVisibleCubeSpans([], 10)).toEqual([])
71
+ expect(buildVisibleCubeSpans([{ start: 0, count: 1 }], 0)).toEqual([])
72
+ })
73
+ })
@@ -0,0 +1,360 @@
1
+ //@ts-nocheck
2
+ import { describe, expect, test } from 'vitest'
3
+ import * as THREE from 'three'
4
+ import { GlobalLegacyBuffer, MAX_OPAQUE_SPANS } from '../globalLegacyBuffer'
5
+ import { createGlobalLegacyBlockMaterial } from '../shaders/legacyBlockShader'
6
+
7
+ function makeQuadGeometry (): {
8
+ positions: Float32Array
9
+ colors: Float32Array
10
+ skyLights: Float32Array
11
+ blockLights: Float32Array
12
+ uvs: Float32Array
13
+ indices: Uint32Array
14
+ } {
15
+ return {
16
+ positions: new Float32Array([
17
+ -1, 0, -1,
18
+ 1, 0, -1,
19
+ 1, 0, 1,
20
+ -1, 0, 1,
21
+ ]),
22
+ colors: new Float32Array([
23
+ 1, 1, 1,
24
+ 1, 1, 1,
25
+ 1, 1, 1,
26
+ 1, 1, 1,
27
+ ]),
28
+ skyLights: new Float32Array([1, 1, 1, 1]),
29
+ blockLights: new Float32Array([0, 0, 0, 0]),
30
+ uvs: new Float32Array([
31
+ 0, 0,
32
+ 1, 0,
33
+ 1, 1,
34
+ 0, 1,
35
+ ]),
36
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3]),
37
+ }
38
+ }
39
+
40
+ type BufferInternals = {
41
+ pendingRanges: Array<{ start: number, end: number }>
42
+ }
43
+
44
+ function getInternals (buffer: GlobalLegacyBuffer): BufferInternals {
45
+ return buffer as unknown as BufferInternals
46
+ }
47
+
48
+ function drainUploads (buffer: GlobalLegacyBuffer): void {
49
+ while (getInternals(buffer).pendingRanges.length) buffer.uploadDirtyRange()
50
+ }
51
+
52
+ test('GlobalLegacyBuffer: slot reuse and a_origin fill', () => {
53
+ const scene = new THREE.Scene()
54
+ const mat = createGlobalLegacyBlockMaterial()
55
+ const buffer = new GlobalLegacyBuffer(mat, scene)
56
+ const geo = makeQuadGeometry()
57
+
58
+ buffer.addSection('a', geo, 100, 64, 200)
59
+ const originAttr = buffer.mesh.geometry.getAttribute('a_origin') as THREE.BufferAttribute
60
+ expect(originAttr.array[0]).toBe(100)
61
+ expect(originAttr.array[1]).toBe(64)
62
+ expect(originAttr.array[2]).toBe(200)
63
+
64
+ buffer.removeSection('a')
65
+ drainUploads(buffer)
66
+
67
+ buffer.addSection('b', geo, 8, 8, 8)
68
+ const indexAttr = buffer.mesh.geometry.index!.array as Uint32Array
69
+ expect(indexAttr[0]).toBe(0)
70
+
71
+ buffer.dispose()
72
+ mat.dispose()
73
+ })
74
+
75
+ test('GlobalLegacyBuffer: a_origin stores world minus render origin', () => {
76
+ const scene = new THREE.Scene()
77
+ const mat = createGlobalLegacyBlockMaterial()
78
+ const buffer = new GlobalLegacyBuffer(mat, scene)
79
+ const geo = makeQuadGeometry()
80
+
81
+ buffer.setRenderOrigin({ x: 16, y: 0, z: 16 })
82
+ buffer.addSection('a', geo, 100, 64, 200)
83
+ const originAttr = buffer.mesh.geometry.getAttribute('a_origin') as THREE.BufferAttribute
84
+ expect(originAttr.array[0]).toBe(84)
85
+ expect(originAttr.array[1]).toBe(64)
86
+ expect(originAttr.array[2]).toBe(184)
87
+
88
+ buffer.dispose()
89
+ mat.dispose()
90
+ })
91
+
92
+ test('GlobalLegacyBuffer: rebase shifts all a_origin and marks dirty', () => {
93
+ const scene = new THREE.Scene()
94
+ const mat = createGlobalLegacyBlockMaterial()
95
+ const buffer = new GlobalLegacyBuffer(mat, scene)
96
+ const geo = makeQuadGeometry()
97
+
98
+ buffer.addSection('a', geo, 100, 64, 200)
99
+ buffer.addSection('b', geo, 16, 8, 16)
100
+
101
+ buffer.rebase({ x: 16, y: 0, z: 16 })
102
+ const originAttr = buffer.mesh.geometry.getAttribute('a_origin') as THREE.BufferAttribute
103
+ expect(originAttr.array[0]).toBe(84)
104
+ expect(originAttr.array[1]).toBe(64)
105
+ expect(originAttr.array[2]).toBe(184)
106
+
107
+ const slotB = buffer.getSectionSlot('b')!
108
+ const baseB = slotB.start * 4 * 3
109
+ expect(originAttr.array[baseB]).toBe(0)
110
+ expect(originAttr.array[baseB + 1]).toBe(8)
111
+ expect(originAttr.array[baseB + 2]).toBe(0)
112
+
113
+ expect(getInternals(buffer).pendingRanges.length).toBeGreaterThan(0)
114
+
115
+ buffer.dispose()
116
+ mat.dispose()
117
+ })
118
+
119
+ test('GlobalLegacyBuffer: index rebase on copy', () => {
120
+ const scene = new THREE.Scene()
121
+ const mat = createGlobalLegacyBlockMaterial()
122
+ const buffer = new GlobalLegacyBuffer(mat, scene)
123
+ const geo = makeQuadGeometry()
124
+
125
+ buffer.addSection('a', geo, 0, 0, 0)
126
+ buffer.addSection('b', geo, 16, 8, 16)
127
+
128
+ const slot = buffer.getSectionSlot('b')!
129
+ const indexAttr = buffer.mesh.geometry.index!.array as Uint32Array
130
+ const base = slot.start * 4
131
+ expect(indexAttr[slot.start * 6]).toBe(base)
132
+
133
+ buffer.dispose()
134
+ mat.dispose()
135
+ })
136
+
137
+ test('GlobalLegacyBuffer: upload budget splits large dirty span', () => {
138
+ const scene = new THREE.Scene()
139
+ const mat = createGlobalLegacyBlockMaterial()
140
+ const buffer = new GlobalLegacyBuffer(mat, scene)
141
+ const geo = makeQuadGeometry()
142
+
143
+ for (let i = 0; i < 12; i++) {
144
+ buffer.addSection(`s${i}`, geo, i, 0, i)
145
+ }
146
+
147
+ const pendingBefore = getInternals(buffer).pendingRanges.length
148
+ expect(pendingBefore).toBeGreaterThan(0)
149
+
150
+ buffer.uploadDirtyRange()
151
+ expect(getInternals(buffer).pendingRanges.length).toBeGreaterThanOrEqual(0)
152
+
153
+ drainUploads(buffer)
154
+ buffer.dispose()
155
+ mat.dispose()
156
+ })
157
+
158
+ test('GlobalLegacyBuffer: removeSection zero-fills indices', () => {
159
+ const scene = new THREE.Scene()
160
+ const mat = createGlobalLegacyBlockMaterial()
161
+ const buffer = new GlobalLegacyBuffer(mat, scene)
162
+ const geo = makeQuadGeometry()
163
+
164
+ buffer.addSection('a', geo, 0, 0, 0)
165
+ const indexAttr = buffer.mesh.geometry.index!.array as Uint32Array
166
+ expect(indexAttr[1]).toBe(1)
167
+
168
+ buffer.removeSection('a')
169
+ expect(indexAttr[0]).toBe(0)
170
+ expect(indexAttr[1]).toBe(0)
171
+ expect(indexAttr[2]).toBe(0)
172
+
173
+ buffer.dispose()
174
+ mat.dispose()
175
+ })
176
+
177
+ test('GlobalLegacyBuffer: material array on mesh', () => {
178
+ const scene = new THREE.Scene()
179
+ const mat = createGlobalLegacyBlockMaterial()
180
+ const buffer = new GlobalLegacyBuffer(mat, scene)
181
+ expect(Array.isArray(buffer.mesh.material)).toBe(true)
182
+ buffer.dispose()
183
+ mat.dispose()
184
+ })
185
+
186
+ test('GlobalLegacyBuffer: updateDrawSpans opaque merges nearby spans', () => {
187
+ const scene = new THREE.Scene()
188
+ const mat = createGlobalLegacyBlockMaterial()
189
+ const buffer = new GlobalLegacyBuffer(mat, scene, { initialCapacityQuads: 16, growthIncrementQuads: 16 })
190
+ const geo = makeQuadGeometry()
191
+
192
+ buffer.addSection('a', geo, 0, 0, 0)
193
+ buffer.addSection('b', geo, 16, 0, 0)
194
+ buffer.updateDrawSpans([{ key: 'a', distSq: 1 }, { key: 'b', distSq: 4 }], 'opaque')
195
+
196
+ const groups = buffer.mesh.geometry.groups
197
+ expect(groups.length).toBe(1)
198
+ expect(groups[0]!.count).toBe(12)
199
+
200
+ buffer.dispose()
201
+ mat.dispose()
202
+ })
203
+
204
+ test('GlobalLegacyBuffer: updateDrawSpans opaque full draw when most quads visible', () => {
205
+ const scene = new THREE.Scene()
206
+ const mat = createGlobalLegacyBlockMaterial()
207
+ const buffer = new GlobalLegacyBuffer(mat, scene, { initialCapacityQuads: 4, growthIncrementQuads: 4 })
208
+ const geo = makeQuadGeometry()
209
+
210
+ buffer.addSection('a', geo, 0, 0, 0)
211
+ buffer.addSection('b', geo, 16, 0, 0)
212
+ buffer.addSection('c', geo, 32, 0, 0)
213
+ buffer.updateDrawSpans([{ key: 'a', distSq: 1 }, { key: 'b', distSq: 2 }, { key: 'c', distSq: 3 }], 'opaque')
214
+
215
+ const groups = buffer.mesh.geometry.groups
216
+ expect(groups.length).toBe(1)
217
+ expect(groups[0]!.start).toBe(0)
218
+ expect(groups[0]!.count).toBe(18)
219
+
220
+ buffer.dispose()
221
+ mat.dispose()
222
+ })
223
+
224
+ test('GlobalLegacyBuffer: updateDrawSpans sortedBlend orders back-to-front', () => {
225
+ const scene = new THREE.Scene()
226
+ const mat = createGlobalLegacyBlockMaterial()
227
+ const buffer = new GlobalLegacyBuffer(mat, scene, { initialCapacityQuads: 16, growthIncrementQuads: 16 })
228
+ const geo = makeQuadGeometry()
229
+
230
+ buffer.addSection('near', geo, 0, 0, 0)
231
+ buffer.addSection('far', geo, 16, 0, 0)
232
+ buffer.updateDrawSpans([
233
+ { key: 'near', distSq: 1 },
234
+ { key: 'far', distSq: 100 },
235
+ ], 'sortedBlend')
236
+
237
+ const groups = buffer.mesh.geometry.groups
238
+ expect(groups.length).toBe(2)
239
+ expect(groups[0]!.start).toBe(6)
240
+ expect(groups[1]!.start).toBe(0)
241
+
242
+ buffer.dispose()
243
+ mat.dispose()
244
+ })
245
+
246
+ test('GlobalLegacyBuffer: updateDrawSpans skips missing keys', () => {
247
+ const scene = new THREE.Scene()
248
+ const mat = createGlobalLegacyBlockMaterial()
249
+ const buffer = new GlobalLegacyBuffer(mat, scene)
250
+ const geo = makeQuadGeometry()
251
+
252
+ buffer.addSection('a', geo, 0, 0, 0)
253
+ buffer.updateDrawSpans([{ key: 'missing', distSq: 1 }], 'opaque')
254
+
255
+ expect(buffer.mesh.geometry.groups.length).toBe(0)
256
+ expect(buffer.mesh.geometry.drawRange.count).toBe(0)
257
+
258
+ buffer.dispose()
259
+ mat.dispose()
260
+ })
261
+
262
+ test('GlobalLegacyBuffer: reset clears groups', () => {
263
+ const scene = new THREE.Scene()
264
+ const mat = createGlobalLegacyBlockMaterial()
265
+ const buffer = new GlobalLegacyBuffer(mat, scene)
266
+ const geo = makeQuadGeometry()
267
+
268
+ buffer.addSection('a', geo, 0, 0, 0)
269
+ buffer.updateDrawSpans([{ key: 'a', distSq: 1 }], 'opaque')
270
+ expect(buffer.mesh.geometry.groups.length).toBeGreaterThan(0)
271
+
272
+ buffer.reset()
273
+ expect(buffer.mesh.geometry.groups.length).toBe(0)
274
+
275
+ buffer.dispose()
276
+ mat.dispose()
277
+ })
278
+
279
+ test('GlobalLegacyBuffer: updateDrawSpans opaque caps at MAX_OPAQUE_SPANS with full coverage', () => {
280
+ const scene = new THREE.Scene()
281
+ const mat = createGlobalLegacyBlockMaterial()
282
+ const visibleSectionCount = MAX_OPAQUE_SPANS + 5
283
+ const padQuads = 257
284
+ const buffer = new GlobalLegacyBuffer(mat, scene, {
285
+ initialCapacityQuads: visibleSectionCount * (padQuads + 1) + padQuads,
286
+ growthIncrementQuads: 1024,
287
+ })
288
+ const geo = makeQuadGeometry()
289
+ const padGeo = {
290
+ ...makeQuadGeometry(),
291
+ positions: new Float32Array(padQuads * 4 * 3),
292
+ colors: new Float32Array(padQuads * 4 * 3).fill(1),
293
+ skyLights: new Float32Array(padQuads * 4).fill(1),
294
+ blockLights: new Float32Array(padQuads * 4).fill(0),
295
+ uvs: new Float32Array(padQuads * 4 * 2),
296
+ indices: new Uint32Array(padQuads * 6),
297
+ }
298
+ for (let q = 0; q < padQuads; q++) {
299
+ const vb = q * 4
300
+ padGeo.indices.set([vb, vb + 1, vb + 2, vb, vb + 2, vb + 3], q * 6)
301
+ }
302
+ const visible: Array<{ key: string, distSq: number }> = []
303
+
304
+ for (let i = 0; i < visibleSectionCount; i++) {
305
+ const key = `s${i}`
306
+ buffer.addSection(key, geo, i * 16, 0, 0)
307
+ visible.push({ key, distSq: i })
308
+ if (i < visibleSectionCount - 1) {
309
+ buffer.addSection(`pad${i}`, padGeo, 0, 0, 0)
310
+ }
311
+ }
312
+
313
+ for (let i = 0; i < visibleSectionCount - 1; i++) {
314
+ const cur = buffer.getSectionSlot(`s${i}`)!
315
+ const next = buffer.getSectionSlot(`s${i + 1}`)!
316
+ expect(next.start - (cur.start + cur.count)).toBeGreaterThan(256)
317
+ }
318
+
319
+ buffer.updateDrawSpans(visible, 'opaque')
320
+
321
+ const groups = buffer.mesh.geometry.groups
322
+ expect(groups.length).toBe(MAX_OPAQUE_SPANS)
323
+
324
+ const covered = new Set<number>()
325
+ for (const group of groups) {
326
+ for (let idx = group.start; idx < group.start + group.count; idx++) {
327
+ covered.add(idx)
328
+ }
329
+ }
330
+ for (let i = 0; i < visibleSectionCount; i++) {
331
+ const slot = buffer.getSectionSlot(`s${i}`)!
332
+ const startIdx = slot.start * 6
333
+ const endIdx = startIdx + slot.count * 6
334
+ for (let idx = startIdx; idx < endIdx; idx++) {
335
+ expect(covered.has(idx)).toBe(true)
336
+ }
337
+ }
338
+
339
+ buffer.dispose()
340
+ mat.dispose()
341
+ })
342
+
343
+ test('GlobalLegacyBuffer: addSection rejects non-quad geometry', () => {
344
+ const scene = new THREE.Scene()
345
+ const mat = createGlobalLegacyBlockMaterial()
346
+ const buffer = new GlobalLegacyBuffer(mat, scene)
347
+
348
+ const bad = {
349
+ positions: new Float32Array([0, 0, 0, 1, 0, 0, 2, 0, 0]),
350
+ colors: new Float32Array(9),
351
+ skyLights: new Float32Array(3).fill(1),
352
+ blockLights: new Float32Array(3).fill(0),
353
+ uvs: new Float32Array(6),
354
+ indices: new Uint32Array([0, 1, 2]),
355
+ }
356
+ expect(buffer.addSection('bad', bad, 0, 0, 0)).toBe(false)
357
+
358
+ buffer.dispose()
359
+ mat.dispose()
360
+ })