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.
- package/README.md +1 -1
- package/dist/mesher.js +81 -81
- package/dist/mesher.js.map +3 -3
- package/dist/mesherWasm.js +1183 -943
- package/dist/minecraft-renderer.js +253 -80
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +1735 -1002
- package/package.json +3 -3
- package/src/graphicsBackend/config.ts +4 -0
- package/src/graphicsBackend/rendererDefaultOptions.ts +2 -7
- package/src/graphicsBackend/rendererOptionsSync.ts +1 -1
- package/src/graphicsBackend/types.ts +1 -0
- package/src/lib/bakeLegacyLight.ts +17 -0
- package/src/lib/bindAbortableListener.ts +1 -1
- package/src/lib/blockEntityLightRegistry.test.ts +18 -0
- package/src/lib/blockEntityLightRegistry.ts +75 -0
- package/src/lib/blockEntityLighting.test.ts +30 -0
- package/src/lib/blockEntityLighting.ts +53 -0
- package/src/lib/createPlayerObject.ts +1 -1
- package/src/lib/worldrendererCommon.reconfigure.test.ts +4 -1
- package/src/lib/worldrendererCommon.removeColumn.test.ts +8 -4
- package/src/lib/worldrendererCommon.ts +15 -7
- package/src/mesher-shared/blockEntityMetadata.test.ts +33 -0
- package/src/mesher-shared/blockEntityMetadata.ts +19 -3
- package/src/mesher-shared/exportedGeometryTypes.ts +11 -0
- package/src/mesher-shared/models.ts +161 -92
- package/src/mesher-shared/shared.ts +15 -4
- package/src/mesher-shared/tests/liquidQuadInvariant.test.ts +40 -0
- package/src/mesher-shared/world.ts +12 -0
- package/src/mesher-shared/worldLighting.test.ts +54 -0
- package/src/playground/baseScene.ts +1 -1
- package/src/three/bannerRenderer.ts +14 -4
- package/src/three/chunkMeshManager.ts +663 -69
- package/src/three/cubeDrawSpans.ts +74 -0
- package/src/three/cubeMultiDraw.ts +119 -0
- package/src/three/documentRenderer.ts +0 -2
- package/src/three/entities.ts +7 -7
- package/src/three/entity/EntityMesh.ts +7 -5
- package/src/three/entity/gltfAnimationUtils.ts +5 -3
- package/src/three/globalBlockBuffer.ts +208 -12
- package/src/three/globalLegacyBuffer.ts +701 -0
- package/src/three/graphicsBackendBase.ts +9 -5
- package/src/three/itemMesh.ts +6 -3
- package/src/three/legacySectionCull.ts +85 -0
- package/src/three/modules/rain.ts +22 -21
- package/src/three/modules/sciFiWorldReveal.ts +347 -703
- package/src/three/modules/starfield.ts +19 -6
- package/src/three/sectionRaycastAabb.ts +25 -0
- package/src/three/shaders/cubeBlockShader.ts +80 -17
- package/src/three/shaders/legacyBlockShader.ts +292 -0
- package/src/three/skyboxRenderer.ts +1 -1
- package/src/three/tests/chunkMeshManagerLegacy.test.ts +286 -0
- package/src/three/tests/cubeDrawSpans.test.ts +73 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +360 -0
- package/src/three/tests/legacySectionCull.test.ts +80 -0
- package/src/three/tests/signTextureCache.test.ts +83 -0
- package/src/three/threeJsMedia.ts +2 -2
- package/src/three/waypointSprite.ts +2 -2
- package/src/three/world/cursorBlock.ts +1 -0
- package/src/three/world/vr.ts +2 -2
- package/src/three/worldGeometryExport.ts +83 -26
- package/src/three/worldRendererThree.ts +100 -30
- package/src/wasm-mesher/bridge/render-from-wasm.ts +214 -72
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +18 -6
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +20 -0
- package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +80 -12
- package/src/wasm-mesher/worker/mesherWasm.ts +70 -14
- package/src/wasm-mesher/worker/mesherWasmLightDirty.test.ts +11 -0
- 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
|
+
})
|