minecraft-renderer 0.1.87 → 0.1.89
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/dist/mesher.js +20 -20
- package/dist/mesher.js.map +3 -3
- package/dist/mesherWasm.js +165 -165
- package/dist/minecraft-renderer.js +28 -28
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +355 -355
- package/package.json +1 -1
- package/src/mesher-shared/faceOcclusion.ts +56 -12
- package/src/mesher-shared/tests/faceOcclusion.test.ts +36 -0
- package/src/playground/scenes/partialBlockCulling.ts +14 -1
- package/src/three/chunkMeshManager.ts +35 -1
- package/src/three/cubeDrawSpans.ts +1 -9
- package/src/three/globalBlockBuffer.ts +0 -2
- package/src/three/globalLegacyBuffer.ts +174 -31
- package/src/three/shaders/legacyBlockShader.ts +1 -1
- package/src/three/tests/cubeDrawSpans.test.ts +31 -0
- package/src/three/tests/globalLegacyBuffer.test.ts +132 -0
- package/src/three/worldRendererThree.ts +6 -0
- package/src/wasm-mesher/tests/cullingRegression.test.ts +26 -0
package/package.json
CHANGED
|
@@ -253,19 +253,21 @@ function getBlockFromStateId(version: string, stateId: number) {
|
|
|
253
253
|
return Block.fromStateId(stateId, 1)
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
function
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return
|
|
256
|
+
function isFullCubeRenderModel(models: BlockModelPartsResolved | undefined): boolean {
|
|
257
|
+
try {
|
|
258
|
+
if (!models?.length || models.length !== 1) return false
|
|
259
|
+
return models[0]!.every(v =>
|
|
260
|
+
(v.elements ?? []).every(e => {
|
|
261
|
+
return e.from[0] === 0 && e.from[1] === 0 && e.from[2] === 0 && e.to[0] === 16 && e.to[1] === 16 && e.to[2] === 16
|
|
262
|
+
})
|
|
263
|
+
)
|
|
264
|
+
} catch {
|
|
265
|
+
return false
|
|
266
266
|
}
|
|
267
|
+
}
|
|
267
268
|
|
|
268
|
-
|
|
269
|
+
function buildShapesFromRenderModel(models: BlockModelPartsResolved | undefined): Uint16Array[] {
|
|
270
|
+
const shapes = DIR_KEYS.map(() => emptyShape())
|
|
269
271
|
|
|
270
272
|
for (const modelVars of models ?? []) {
|
|
271
273
|
const model = modelVars[0]
|
|
@@ -289,10 +291,52 @@ function getAllShapesForState(version: string, stateId: number, blockProvider: W
|
|
|
289
291
|
}
|
|
290
292
|
}
|
|
291
293
|
|
|
292
|
-
shapeCache.set(cacheKey, shapes)
|
|
293
294
|
return shapes
|
|
294
295
|
}
|
|
295
296
|
|
|
297
|
+
function buildShapesFromCollisionBoxes(boxes: number[][]): Uint16Array[] {
|
|
298
|
+
const shapes = DIR_KEYS.map(() => emptyShape())
|
|
299
|
+
|
|
300
|
+
for (const box of boxes) {
|
|
301
|
+
const x0 = snapBlockUnit(box[0]! * 16)
|
|
302
|
+
const y0 = snapBlockUnit(box[1]! * 16)
|
|
303
|
+
const z0 = snapBlockUnit(box[2]! * 16)
|
|
304
|
+
const x1 = snapBlockUnit(box[3]! * 16)
|
|
305
|
+
const y1 = snapBlockUnit(box[4]! * 16)
|
|
306
|
+
const z1 = snapBlockUnit(box[5]! * 16)
|
|
307
|
+
|
|
308
|
+
if (y1 === 16) orRectIntoShape(x0, z0, x1, z1, shapes[dirIndex([0, 1, 0])]!)
|
|
309
|
+
if (y0 === 0) orRectIntoShape(x0, z0, x1, z1, shapes[dirIndex([0, -1, 0])]!)
|
|
310
|
+
if (x1 === 16) orRectIntoShape(z0, y0, z1, y1, shapes[dirIndex([1, 0, 0])]!)
|
|
311
|
+
if (x0 === 0) orRectIntoShape(z0, y0, z1, y1, shapes[dirIndex([-1, 0, 0])]!)
|
|
312
|
+
if (z1 === 16) orRectIntoShape(x0, y0, x1, y1, shapes[dirIndex([0, 0, 1])]!)
|
|
313
|
+
if (z0 === 0) orRectIntoShape(x0, y0, x1, y1, shapes[dirIndex([0, 0, -1])]!)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return shapes
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function getAllShapesForState(version: string, stateId: number, blockProvider: WorldBlockProvider): Uint16Array[] {
|
|
320
|
+
const cacheKey = `${version}:${stateId}`
|
|
321
|
+
const cached = shapeCache.get(cacheKey)
|
|
322
|
+
if (cached) return cached
|
|
323
|
+
|
|
324
|
+
const shapes = DIR_KEYS.map(() => emptyShape())
|
|
325
|
+
const blockObj = getBlockFromStateId(version, stateId)
|
|
326
|
+
if (!blockObj || !blockRendersSolid(blockObj)) {
|
|
327
|
+
shapeCache.set(cacheKey, shapes)
|
|
328
|
+
return shapes
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const models = blockProvider.getAllResolvedModels0_1({ name: blockObj.name, properties: blockObj.getProperties() }, false) as BlockModelPartsResolved
|
|
332
|
+
const boxes = blockObj.shapes as number[][] | undefined
|
|
333
|
+
|
|
334
|
+
const resolvedShapes = isFullCubeRenderModel(models) ? buildShapesFromRenderModel(models) : boxes?.length ? buildShapesFromCollisionBoxes(boxes) : shapes
|
|
335
|
+
|
|
336
|
+
shapeCache.set(cacheKey, resolvedShapes)
|
|
337
|
+
return resolvedShapes
|
|
338
|
+
}
|
|
339
|
+
|
|
296
340
|
export function getOcclusionShape(version: string, stateId: number, worldDir: CardinalDir, blockProvider: WorldBlockProvider): Uint16Array {
|
|
297
341
|
return getAllShapesForState(version, stateId, blockProvider)[dirIndex(worldDir)]!
|
|
298
342
|
}
|
|
@@ -73,6 +73,42 @@ test('getOcclusionShape: full cube covers entire plane', () => {
|
|
|
73
73
|
}
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
+
test('getOcclusionShape: cactus down occlusion is inset, not full footprint', () => {
|
|
77
|
+
const mcData = MinecraftData(VERSION)
|
|
78
|
+
const cactusId = mcData.blocksByName.cactus!.defaultState
|
|
79
|
+
const downShape = getOcclusionShape(VERSION, cactusId, [0, -1, 0], blockProvider())
|
|
80
|
+
expect(downShape[0]! & 1).toBe(0)
|
|
81
|
+
expect(downShape[0]! & (1 << 15)).toBe(0)
|
|
82
|
+
let covered = 0
|
|
83
|
+
for (let row = 0; row < 16; row++) {
|
|
84
|
+
for (let col = 0; col < 16; col++) {
|
|
85
|
+
if (downShape[row]! & (1 << col)) covered++
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
expect(covered).toBe(14 * 14)
|
|
89
|
+
expect(getOcclusionShape(VERSION, cactusId, [0, 1, 0], blockProvider())[0]).toBe(0)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('faceIsCulled: grass top not culled by cactus above', () => {
|
|
93
|
+
const mcData = MinecraftData(VERSION)
|
|
94
|
+
const grassId = mcData.blocksByName.grass_block!.defaultState
|
|
95
|
+
const cactusId = mcData.blocksByName.cactus!.defaultState
|
|
96
|
+
const provider = blockProvider()
|
|
97
|
+
const blockObj = PrismarineBlockLoader(VERSION).fromStateId(grassId, 1)
|
|
98
|
+
const models = provider.getAllResolvedModels0_1({ name: blockObj.name, properties: blockObj.getProperties() }, false)
|
|
99
|
+
const element = models[0]![0]!.elements![0]!
|
|
100
|
+
expect(faceIsCulled(VERSION, element, 'up', cactusId, { stateId: grassId, name: 'grass_block' }, provider, [0, 1, 0], null)).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('getOcclusionShape: soul sand keeps full render-model occlusion despite shorter collision', () => {
|
|
104
|
+
const mcData = MinecraftData(VERSION)
|
|
105
|
+
const soulSandId = mcData.blocksByName.soul_sand!.defaultState
|
|
106
|
+
const downShape = getOcclusionShape(VERSION, soulSandId, [0, -1, 0], blockProvider())
|
|
107
|
+
for (let row = 0; row < 16; row++) {
|
|
108
|
+
expect(downShape[row]).toBe(0xffff)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
76
112
|
test('getOcclusionShape: farmland side is shorter than full height', () => {
|
|
77
113
|
const mcData = MinecraftData(VERSION)
|
|
78
114
|
const farmlandId = mcData.blocksByName.farmland!.defaultState
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { BasePlaygroundScene } from '../baseScene'
|
|
3
3
|
|
|
4
|
-
/** Manual check for issue #49:
|
|
4
|
+
/** Manual check for issue #49/#73: partial-block culling + cactus on grass (1.18.2). */
|
|
5
5
|
export default class extends BasePlaygroundScene {
|
|
6
6
|
override version = '1.18.2'
|
|
7
7
|
enableCameraOrbitControl = true
|
|
@@ -27,5 +27,18 @@ export default class extends BasePlaygroundScene {
|
|
|
27
27
|
this.addWorldBlock(20 + x, stairY, 4, 'cut_copper_stairs', stairProps)
|
|
28
28
|
this.addWorldBlock(20 + x, stairY + 1, 4, 'cut_copper_stairs', stairProps)
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
const cactusY = 64
|
|
32
|
+
for (let x = 0; x < 4; x++) {
|
|
33
|
+
this.addWorldBlock(30 + x, cactusY - 1, 8, 'grass_block')
|
|
34
|
+
this.addWorldBlock(30 + x, cactusY, 8, 'cactus')
|
|
35
|
+
}
|
|
36
|
+
this.addWorldBlock(35, cactusY - 1, 8, 'grass_block')
|
|
37
|
+
this.addWorldBlock(35, cactusY, 8, 'cactus')
|
|
38
|
+
this.addWorldBlock(35, cactusY + 1, 8, 'cactus')
|
|
39
|
+
|
|
40
|
+
this.addWorldBlock(30, cactusY - 1, 10, 'soul_sand')
|
|
41
|
+
this.addWorldBlock(31, cactusY - 1, 10, 'stone_slab', { type: 'bottom' })
|
|
42
|
+
this.addWorldBlock(32, cactusY - 1, 10, 'stone_slab', { type: 'bottom' })
|
|
30
43
|
}
|
|
31
44
|
}
|
|
@@ -159,6 +159,11 @@ export class ChunkMeshManager {
|
|
|
159
159
|
private readonly _lastCullCamQuat = new THREE.Quaternion()
|
|
160
160
|
private readonly _cullViewQuat = new THREE.Quaternion()
|
|
161
161
|
private _cullCamInitialized = false
|
|
162
|
+
/** Visible blend section keys from last cull pass — used by per-quad blend sort. */
|
|
163
|
+
private _visibleBlendKeys: string[] = []
|
|
164
|
+
private readonly _lastBlendSortPos = new THREE.Vector3()
|
|
165
|
+
private _lastBlendSortLayoutVersion = -1
|
|
166
|
+
private _blendSortPosInitialized = false
|
|
162
167
|
/** One instanced mesh for all shader-cube faces (single draw call). */
|
|
163
168
|
globalBlockBuffer: GlobalBlockBuffer | null = null
|
|
164
169
|
globalLegacyBuffer: GlobalLegacyBuffer | null = null
|
|
@@ -328,7 +333,8 @@ export class ChunkMeshManager {
|
|
|
328
333
|
this.globalLegacyBlendBuffer = new GlobalLegacyBuffer(this.getGlobalLegacyBlendShaderMaterial(), this.scene, {
|
|
329
334
|
name: 'globalLegacyBlend',
|
|
330
335
|
initialCapacityQuads: 32_000,
|
|
331
|
-
growthIncrementQuads: 32_000
|
|
336
|
+
growthIncrementQuads: 32_000,
|
|
337
|
+
sortBlend: true
|
|
332
338
|
})
|
|
333
339
|
this.globalLegacyBlendBuffer.setRenderOrigin(this.renderOrigin)
|
|
334
340
|
}
|
|
@@ -459,6 +465,7 @@ export class ChunkMeshManager {
|
|
|
459
465
|
const blendVisible = visible.filter(v => blendBuf?.hasSection(v.key))
|
|
460
466
|
blendVisible.sort((a, b) => b.distSq - a.distSq)
|
|
461
467
|
const blendKeys = blendVisible.map(v => v.key)
|
|
468
|
+
this._visibleBlendKeys = blendKeys
|
|
462
469
|
|
|
463
470
|
const cubeVisibleKeys: string[] = []
|
|
464
471
|
const visibleSlots: Array<{ start: number; count: number }> = []
|
|
@@ -532,6 +539,33 @@ export class ChunkMeshManager {
|
|
|
532
539
|
this.updatePooledLegacyCullState(cameraWorldX, cameraWorldY, cameraWorldZ)
|
|
533
540
|
}
|
|
534
541
|
|
|
542
|
+
private static readonly BLEND_RESORT_DISTANCE = 1.0
|
|
543
|
+
|
|
544
|
+
/** Per-quad back-to-front reorder within visible blend sections; throttled by camera translation. */
|
|
545
|
+
sortVisibleBlendSections(camX: number, camY: number, camZ: number): void {
|
|
546
|
+
const blendBuf = this.globalLegacyBlendBuffer
|
|
547
|
+
if (!blendBuf || this._visibleBlendKeys.length === 0) return
|
|
548
|
+
|
|
549
|
+
const layoutVersion = blendBuf.getLayoutVersion()
|
|
550
|
+
const layoutChanged = layoutVersion !== this._lastBlendSortLayoutVersion
|
|
551
|
+
if (this._blendSortPosInitialized && !layoutChanged) {
|
|
552
|
+
const dx = camX - this._lastBlendSortPos.x
|
|
553
|
+
const dy = camY - this._lastBlendSortPos.y
|
|
554
|
+
const dz = camZ - this._lastBlendSortPos.z
|
|
555
|
+
if (dx * dx + dy * dy + dz * dz < ChunkMeshManager.BLEND_RESORT_DISTANCE * ChunkMeshManager.BLEND_RESORT_DISTANCE) {
|
|
556
|
+
return
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
for (const key of this._visibleBlendKeys) {
|
|
561
|
+
blendBuf.reorderSectionBlendIndices(key, camX, camY, camZ)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
this._lastBlendSortPos.set(camX, camY, camZ)
|
|
565
|
+
this._blendSortPosInitialized = true
|
|
566
|
+
this._lastBlendSortLayoutVersion = layoutVersion
|
|
567
|
+
}
|
|
568
|
+
|
|
535
569
|
private bufferStateKey(): string {
|
|
536
570
|
const b = this.globalBlockBuffer
|
|
537
571
|
const o = this.globalLegacyBuffer
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
|
-
import {
|
|
3
|
-
assertDrawSpansWithinLiveRanges,
|
|
4
|
-
carveSpansAroundPendingRanges,
|
|
5
|
-
FULL_DRAW_VISIBLE_FRACTION,
|
|
6
|
-
MAX_OPAQUE_SPANS,
|
|
7
|
-
type DirtyRange
|
|
8
|
-
} from './globalLegacyBuffer'
|
|
2
|
+
import { carveSpansAroundPendingRanges, FULL_DRAW_VISIBLE_FRACTION, MAX_OPAQUE_SPANS, type DirtyRange } from './globalLegacyBuffer'
|
|
9
3
|
|
|
10
4
|
export { FULL_DRAW_VISIBLE_FRACTION, MAX_OPAQUE_SPANS as MAX_CUBE_SPANS }
|
|
11
5
|
|
|
@@ -54,10 +48,8 @@ export function buildVisibleCubeSpans(
|
|
|
54
48
|
} else {
|
|
55
49
|
spans = visibleSlots.map(s => ({ start: s.start, count: s.count }))
|
|
56
50
|
spans.sort((a, b) => a.start - b.start)
|
|
57
|
-
const liveDrawRanges = spans.map(s => ({ ...s }))
|
|
58
51
|
mergeCubeSpans(spans)
|
|
59
52
|
spans = carveSpansAroundPendingRanges(spans, pendingRanges)
|
|
60
|
-
assertDrawSpansWithinLiveRanges(spans, liveDrawRanges, 'globalBlockBuffer')
|
|
61
53
|
return spans
|
|
62
54
|
}
|
|
63
55
|
return carveSpansAroundPendingRanges(spans, pendingRanges)
|
|
@@ -711,10 +711,8 @@ export class GlobalBlockBuffer {
|
|
|
711
711
|
this.finalizePendingReplace(key)
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
-
console.warn('[globalBlockBuffer] growing faces', this.capacityFaces, '->', '(need', minFaces, ')')
|
|
715
714
|
let newCap = this.capacityFaces
|
|
716
715
|
while (newCap < minFaces) newCap += GROWTH_INCREMENT_FACES
|
|
717
|
-
console.warn('[globalBlockBuffer] growing faces', this.capacityFaces, '->', newCap)
|
|
718
716
|
|
|
719
717
|
const nw0 = new Uint32Array(newCap)
|
|
720
718
|
const nw1 = new Uint32Array(newCap)
|
|
@@ -32,33 +32,6 @@ export const FULL_DRAW_VISIBLE_FRACTION = 0.75
|
|
|
32
32
|
/** Initial multi_draw scratch size; arrays auto-grow — not a draw-call cap. */
|
|
33
33
|
export const MAX_OPAQUE_SPANS = 64
|
|
34
34
|
|
|
35
|
-
/** Dev assert: every quad in draw spans must lie in a live section's drawable range. */
|
|
36
|
-
export function assertDrawSpansWithinLiveRanges(
|
|
37
|
-
spans: ReadonlyArray<{ start: number; count: number }>,
|
|
38
|
-
liveRanges: ReadonlyArray<{ start: number; count: number }>,
|
|
39
|
-
bufferName: string
|
|
40
|
-
): void {
|
|
41
|
-
for (const span of spans) {
|
|
42
|
-
for (let q = span.start; q < span.start + span.count; q++) {
|
|
43
|
-
let inLive = false
|
|
44
|
-
for (const live of liveRanges) {
|
|
45
|
-
if (q >= live.start && q < live.start + live.count) {
|
|
46
|
-
inLive = true
|
|
47
|
-
break
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (!inLive) {
|
|
51
|
-
console.error('[GlobalLegacyBuffer] draw span covers non-live quad', {
|
|
52
|
-
buffer: bufferName,
|
|
53
|
-
quad: q,
|
|
54
|
-
span,
|
|
55
|
-
liveRanges
|
|
56
|
-
})
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
35
|
export type DirtyRange = { start: number; end: number }
|
|
63
36
|
|
|
64
37
|
/** Split draw spans to exclude quad/face ranges still in pendingRanges (not yet on GPU). */
|
|
@@ -96,6 +69,8 @@ export type GlobalLegacyBufferOptions = {
|
|
|
96
69
|
name?: string
|
|
97
70
|
initialCapacityQuads?: number
|
|
98
71
|
growthIncrementQuads?: number
|
|
72
|
+
/** When true, pre-sort blend indices on addSection and support per-quad reorder. */
|
|
73
|
+
sortBlend?: boolean
|
|
99
74
|
}
|
|
100
75
|
|
|
101
76
|
export type VisibleSectionSpan = { key: string; distSq: number }
|
|
@@ -126,6 +101,7 @@ export class GlobalLegacyBuffer {
|
|
|
126
101
|
readonly material: THREE.ShaderMaterial
|
|
127
102
|
|
|
128
103
|
private readonly growthIncrementQuads: number
|
|
104
|
+
private readonly blendSort: boolean
|
|
129
105
|
private capacityQuads: number
|
|
130
106
|
private positions: Float32Array
|
|
131
107
|
private colors: Float32Array
|
|
@@ -134,10 +110,15 @@ export class GlobalLegacyBuffer {
|
|
|
134
110
|
private uvs: Float32Array
|
|
135
111
|
private aOrigin: Float32Array
|
|
136
112
|
private indices: Uint32Array
|
|
113
|
+
/** Section-relative world centroid per physical quad (3 floats each). */
|
|
114
|
+
private quadCentroids: Float32Array
|
|
115
|
+
/** Per-quad local index template (6 bytes each, values 0..3). */
|
|
116
|
+
private quadIndexTemplate: Uint8Array
|
|
137
117
|
private readonly sectionSlots = new Map<string, { start: number; count: number }>()
|
|
138
118
|
private freeList: Array<{ start: number; count: number }> = []
|
|
139
119
|
private highWatermark = 0
|
|
140
120
|
private pendingRanges: Array<{ start: number; end: number }> = []
|
|
121
|
+
private indexPendingRanges: Array<{ start: number; end: number }> = []
|
|
141
122
|
private readonly _spanScratch: Array<{ start: number; count: number }> = []
|
|
142
123
|
private renderOrigin: RenderOrigin = { x: 0, y: 0, z: 0 }
|
|
143
124
|
private layoutVersion = 0
|
|
@@ -146,12 +127,20 @@ export class GlobalLegacyBuffer {
|
|
|
146
127
|
private uploadEpoch = 0
|
|
147
128
|
private visibleIndexSpans: LegacyDrawSpan[] = []
|
|
148
129
|
private readonly _drawScratch: LegacyMultiDrawScratch = createLegacyMultiDrawScratch()
|
|
130
|
+
/** Reusable scratch for applyBlendSort; grown on demand, never allocated per call. */
|
|
131
|
+
private _sortOrder: Uint32Array = new Uint32Array(0)
|
|
132
|
+
private _sortDistSq: Float64Array = new Float64Array(0)
|
|
149
133
|
private multiDrawCaps: LegacyMultiDrawCaps | null = null
|
|
150
134
|
private debugOverlay = false
|
|
135
|
+
private _camX = 0
|
|
136
|
+
private _camY = 0
|
|
137
|
+
private _camZ = 0
|
|
138
|
+
private _camInitialized = false
|
|
151
139
|
|
|
152
140
|
constructor(material: THREE.ShaderMaterial, scene: THREE.Object3D, opts?: GlobalLegacyBufferOptions) {
|
|
153
141
|
this.material = material
|
|
154
142
|
this.growthIncrementQuads = opts?.growthIncrementQuads ?? DEFAULT_GROWTH_INCREMENT_QUADS
|
|
143
|
+
this.blendSort = opts?.sortBlend ?? false
|
|
155
144
|
this.capacityQuads = opts?.initialCapacityQuads ?? DEFAULT_INITIAL_CAPACITY_QUADS
|
|
156
145
|
const maxVerts = this.capacityQuads * VERTS_PER_QUAD
|
|
157
146
|
this.positions = new Float32Array(maxVerts * FLOATS_PER_VERT)
|
|
@@ -161,6 +150,8 @@ export class GlobalLegacyBuffer {
|
|
|
161
150
|
this.uvs = new Float32Array(maxVerts * FLOATS_PER_UV_VERT)
|
|
162
151
|
this.aOrigin = new Float32Array(maxVerts * FLOATS_PER_VERT)
|
|
163
152
|
this.indices = new Uint32Array(this.capacityQuads * INDICES_PER_QUAD)
|
|
153
|
+
this.quadCentroids = new Float32Array(this.capacityQuads * 3)
|
|
154
|
+
this.quadIndexTemplate = new Uint8Array(this.capacityQuads * INDICES_PER_QUAD)
|
|
164
155
|
|
|
165
156
|
const geometry = new THREE.BufferGeometry()
|
|
166
157
|
const mkAttr = (arr: Float32Array, itemSize: number, name: string) => {
|
|
@@ -305,10 +296,38 @@ export class GlobalLegacyBuffer {
|
|
|
305
296
|
this.indices[dstIndexBase + i] = geo.indices[i]! + vertexBase
|
|
306
297
|
}
|
|
307
298
|
|
|
299
|
+
for (let q = 0; q < quadCount; q++) {
|
|
300
|
+
const physQuad = slot.start + q
|
|
301
|
+
const localVertBase = q * VERTS_PER_QUAD
|
|
302
|
+
const posBase = localVertBase * FLOATS_PER_VERT
|
|
303
|
+
let cx = 0
|
|
304
|
+
let cy = 0
|
|
305
|
+
let cz = 0
|
|
306
|
+
for (let v = 0; v < VERTS_PER_QUAD; v++) {
|
|
307
|
+
const p = posBase + v * FLOATS_PER_VERT
|
|
308
|
+
cx += geo.positions[p]!
|
|
309
|
+
cy += geo.positions[p + 1]!
|
|
310
|
+
cz += geo.positions[p + 2]!
|
|
311
|
+
}
|
|
312
|
+
const centBase = physQuad * 3
|
|
313
|
+
this.quadCentroids[centBase] = cx / VERTS_PER_QUAD
|
|
314
|
+
this.quadCentroids[centBase + 1] = cy / VERTS_PER_QUAD
|
|
315
|
+
this.quadCentroids[centBase + 2] = cz / VERTS_PER_QUAD
|
|
316
|
+
|
|
317
|
+
const idxBase = q * INDICES_PER_QUAD
|
|
318
|
+
const tmplBase = physQuad * INDICES_PER_QUAD
|
|
319
|
+
for (let i = 0; i < INDICES_PER_QUAD; i++) {
|
|
320
|
+
this.quadIndexTemplate[tmplBase + i] = geo.indices[idxBase + i]! - localVertBase
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
308
324
|
this.sectionSlots.set(sectionKey, slot)
|
|
309
325
|
if (isRemesh && previousSlot) {
|
|
310
326
|
this.pendingReplace.set(sectionKey, { oldStart: previousSlot.start, oldCount: previousSlot.count })
|
|
311
327
|
}
|
|
328
|
+
if (this.blendSort && this._camInitialized && quadCount >= 2) {
|
|
329
|
+
this.applyBlendSort(slot.start, quadCount, this._camX, this._camY, this._camZ)
|
|
330
|
+
}
|
|
312
331
|
this.markDirty(slot.start, slot.start + quadCount - 1)
|
|
313
332
|
this.syncDefaultDrawGroups()
|
|
314
333
|
this.layoutVersion++
|
|
@@ -435,7 +454,6 @@ export class GlobalLegacyBuffer {
|
|
|
435
454
|
|
|
436
455
|
if (mode === 'opaque') {
|
|
437
456
|
let finalQuads: Array<{ start: number; count: number }>
|
|
438
|
-
const liveDrawRanges = spans.map(s => ({ ...s }))
|
|
439
457
|
const usedFullDraw = this.canUseFullDrawShortcut() && visibleQuadCount >= this.highWatermark * FULL_DRAW_VISIBLE_FRACTION
|
|
440
458
|
if (usedFullDraw) {
|
|
441
459
|
finalQuads = [{ start: 0, count: this.highWatermark }]
|
|
@@ -445,9 +463,6 @@ export class GlobalLegacyBuffer {
|
|
|
445
463
|
finalQuads = spans
|
|
446
464
|
}
|
|
447
465
|
finalQuads = carveSpansAroundPendingRanges(finalQuads, this.pendingRanges)
|
|
448
|
-
if (!usedFullDraw) {
|
|
449
|
-
assertDrawSpansWithinLiveRanges(finalQuads, liveDrawRanges, this.mesh.name)
|
|
450
|
-
}
|
|
451
466
|
for (const span of finalQuads) {
|
|
452
467
|
pushIndexSpan(span.start, span.count)
|
|
453
468
|
}
|
|
@@ -558,6 +573,70 @@ export class GlobalLegacyBuffer {
|
|
|
558
573
|
return this.pendingRanges.length > 0
|
|
559
574
|
}
|
|
560
575
|
|
|
576
|
+
hasPendingIndexUploads(): boolean {
|
|
577
|
+
return this.indexPendingRanges.length > 0
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Reorder a section's index buffer back-to-front by quad centroid distance to camera.
|
|
582
|
+
* Does not bump layoutVersion or uploadEpoch (draw spans unchanged).
|
|
583
|
+
*/
|
|
584
|
+
reorderSectionBlendIndices(sectionKey: string, camX: number, camY: number, camZ: number): boolean {
|
|
585
|
+
const slot = this.sectionSlots.get(sectionKey)
|
|
586
|
+
if (!slot) return false
|
|
587
|
+
if (this.pendingMove?.key === sectionKey) return false
|
|
588
|
+
if (this.pendingReplace.has(sectionKey)) return false
|
|
589
|
+
if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return false
|
|
590
|
+
if (slot.count < 2) return false
|
|
591
|
+
|
|
592
|
+
this.applyBlendSort(slot.start, slot.count, camX, camY, camZ)
|
|
593
|
+
this.markIndexDirty(slot.start, slot.start + slot.count - 1)
|
|
594
|
+
return true
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Rewrite indices for [slotStart, slotStart+slotCount) back-to-front by quad centroid distance. */
|
|
598
|
+
private applyBlendSort(slotStart: number, slotCount: number, camX: number, camY: number, camZ: number): void {
|
|
599
|
+
const dstFloatBase = slotStart * VERTS_PER_QUAD * FLOATS_PER_VERT
|
|
600
|
+
const sx = this.aOrigin[dstFloatBase]! + this.renderOrigin.x
|
|
601
|
+
const sy = this.aOrigin[dstFloatBase + 1]! + this.renderOrigin.y
|
|
602
|
+
const sz = this.aOrigin[dstFloatBase + 2]! + this.renderOrigin.z
|
|
603
|
+
|
|
604
|
+
if (this._sortOrder.length < slotCount) {
|
|
605
|
+
this._sortOrder = new Uint32Array(slotCount)
|
|
606
|
+
this._sortDistSq = new Float64Array(slotCount)
|
|
607
|
+
}
|
|
608
|
+
const order = this._sortOrder
|
|
609
|
+
const distSq = this._sortDistSq
|
|
610
|
+
for (let p = 0; p < slotCount; p++) {
|
|
611
|
+
order[p] = p
|
|
612
|
+
const physQuad = slotStart + p
|
|
613
|
+
const centBase = physQuad * 3
|
|
614
|
+
const wx = sx + this.quadCentroids[centBase]!
|
|
615
|
+
const wy = sy + this.quadCentroids[centBase + 1]!
|
|
616
|
+
const wz = sz + this.quadCentroids[centBase + 2]!
|
|
617
|
+
const dx = wx - camX
|
|
618
|
+
const dy = wy - camY
|
|
619
|
+
const dz = wz - camZ
|
|
620
|
+
distSq[p] = dx * dx + dy * dy + dz * dz
|
|
621
|
+
}
|
|
622
|
+
const orderView = order.subarray(0, slotCount)
|
|
623
|
+
orderView.sort((a, b) => {
|
|
624
|
+
const d = distSq[b]! - distSq[a]!
|
|
625
|
+
if (d !== 0) return d
|
|
626
|
+
return a - b
|
|
627
|
+
})
|
|
628
|
+
|
|
629
|
+
for (let k = 0; k < slotCount; k++) {
|
|
630
|
+
const src = orderView[k]!
|
|
631
|
+
const srcVertBase = (slotStart + src) * VERTS_PER_QUAD
|
|
632
|
+
const dstIdxBase = (slotStart + k) * INDICES_PER_QUAD
|
|
633
|
+
const tmplBase = (slotStart + src) * INDICES_PER_QUAD
|
|
634
|
+
for (let i = 0; i < INDICES_PER_QUAD; i++) {
|
|
635
|
+
this.indices[dstIdxBase + i] = srcVertBase + this.quadIndexTemplate[tmplBase + i]!
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
561
640
|
uploadDirtyRange(): void {
|
|
562
641
|
const r = this.pendingRanges[0]
|
|
563
642
|
if (!r) return
|
|
@@ -613,6 +692,27 @@ export class GlobalLegacyBuffer {
|
|
|
613
692
|
this.uploadEpoch++
|
|
614
693
|
}
|
|
615
694
|
|
|
695
|
+
uploadDirtyIndexRange(): void {
|
|
696
|
+
const r = this.indexPendingRanges[0]
|
|
697
|
+
if (!r) return
|
|
698
|
+
|
|
699
|
+
const quadOffset = r.start
|
|
700
|
+
const quadCount = Math.min(r.end - r.start + 1, MAX_UPLOAD_QUADS_PER_FRAME)
|
|
701
|
+
const indexOffset = quadOffset * INDICES_PER_QUAD
|
|
702
|
+
const indexCount = quadCount * INDICES_PER_QUAD
|
|
703
|
+
|
|
704
|
+
const indexAttr = this.mesh.geometry.index as THREE.BufferAttribute
|
|
705
|
+
indexAttr.clearUpdateRanges()
|
|
706
|
+
indexAttr.addUpdateRange(indexOffset, indexCount)
|
|
707
|
+
indexAttr.needsUpdate = true
|
|
708
|
+
|
|
709
|
+
if (quadOffset + quadCount > r.end) {
|
|
710
|
+
this.indexPendingRanges.shift()
|
|
711
|
+
} else {
|
|
712
|
+
r.start = quadOffset + quadCount
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
616
716
|
setRenderOrigin(renderOrigin: RenderOrigin): void {
|
|
617
717
|
this.renderOrigin = { ...renderOrigin }
|
|
618
718
|
}
|
|
@@ -637,6 +737,10 @@ export class GlobalLegacyBuffer {
|
|
|
637
737
|
}
|
|
638
738
|
|
|
639
739
|
setCameraOrigin(x: number, y: number, z: number): void {
|
|
740
|
+
this._camX = x
|
|
741
|
+
this._camY = y
|
|
742
|
+
this._camZ = z
|
|
743
|
+
this._camInitialized = true
|
|
640
744
|
const { originDelta, cameraOriginFrac } = computeCameraRelativeUniforms(this.renderOrigin, x, y, z)
|
|
641
745
|
const u = this.material.uniforms.u_originDelta
|
|
642
746
|
if (u?.value?.set) u.value.set(originDelta.x, originDelta.y, originDelta.z)
|
|
@@ -669,6 +773,7 @@ export class GlobalLegacyBuffer {
|
|
|
669
773
|
this.freeList.length = 0
|
|
670
774
|
this.highWatermark = 0
|
|
671
775
|
this.pendingRanges.length = 0
|
|
776
|
+
this.indexPendingRanges.length = 0
|
|
672
777
|
this.pendingMove = null
|
|
673
778
|
this.pendingReplace.clear()
|
|
674
779
|
this.uploadEpoch = 0
|
|
@@ -688,6 +793,29 @@ export class GlobalLegacyBuffer {
|
|
|
688
793
|
this.mergePendingRanges()
|
|
689
794
|
}
|
|
690
795
|
|
|
796
|
+
private markIndexDirty(start: number, end: number): void {
|
|
797
|
+
this.indexPendingRanges.push({ start, end })
|
|
798
|
+
this.indexPendingRanges.sort((a, b) => a.start - b.start)
|
|
799
|
+
this.mergeIndexPendingRanges()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private mergeIndexPendingRanges(): void {
|
|
803
|
+
if (this.indexPendingRanges.length < 2) return
|
|
804
|
+
const merged: Array<{ start: number; end: number }> = []
|
|
805
|
+
let cur = this.indexPendingRanges[0]!
|
|
806
|
+
for (let i = 1; i < this.indexPendingRanges.length; i++) {
|
|
807
|
+
const next = this.indexPendingRanges[i]!
|
|
808
|
+
if (next.start <= cur.end + 1) {
|
|
809
|
+
cur = { start: cur.start, end: Math.max(cur.end, next.end) }
|
|
810
|
+
} else {
|
|
811
|
+
merged.push(cur)
|
|
812
|
+
cur = next
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
merged.push(cur)
|
|
816
|
+
this.indexPendingRanges = merged
|
|
817
|
+
}
|
|
818
|
+
|
|
691
819
|
private mergePendingRanges(): void {
|
|
692
820
|
if (this.pendingRanges.length < 2) return
|
|
693
821
|
const merged: Array<{ start: number; end: number }> = []
|
|
@@ -835,6 +963,14 @@ export class GlobalLegacyBuffer {
|
|
|
835
963
|
for (let i = 0; i < indexLen; i++) {
|
|
836
964
|
this.indices[newIndexBase + i] = this.indices[oldIndexBase + i]! + vertDelta
|
|
837
965
|
}
|
|
966
|
+
|
|
967
|
+
const oldCentroidBase = oldStart * 3
|
|
968
|
+
const newCentroidBase = newStart * 3
|
|
969
|
+
this.quadCentroids.copyWithin(newCentroidBase, oldCentroidBase, oldCentroidBase + quadCount * 3)
|
|
970
|
+
|
|
971
|
+
const oldTemplateBase = oldStart * INDICES_PER_QUAD
|
|
972
|
+
const newTemplateBase = newStart * INDICES_PER_QUAD
|
|
973
|
+
this.quadIndexTemplate.copyWithin(newTemplateBase, oldTemplateBase, oldTemplateBase + quadCount * INDICES_PER_QUAD)
|
|
838
974
|
}
|
|
839
975
|
|
|
840
976
|
private rangeFullyUploaded(start: number, end: number): boolean {
|
|
@@ -898,6 +1034,8 @@ export class GlobalLegacyBuffer {
|
|
|
898
1034
|
const nUv = new Float32Array(newMaxVerts * FLOATS_PER_UV_VERT)
|
|
899
1035
|
const nOrigin = new Float32Array(newMaxVerts * FLOATS_PER_VERT)
|
|
900
1036
|
const nIdx = new Uint32Array(newCap * INDICES_PER_QUAD)
|
|
1037
|
+
const nCentroids = new Float32Array(newCap * 3)
|
|
1038
|
+
const nTemplate = new Uint8Array(newCap * INDICES_PER_QUAD)
|
|
901
1039
|
|
|
902
1040
|
nPos.set(this.positions)
|
|
903
1041
|
nCol.set(this.colors)
|
|
@@ -906,6 +1044,8 @@ export class GlobalLegacyBuffer {
|
|
|
906
1044
|
nUv.set(this.uvs)
|
|
907
1045
|
nOrigin.set(this.aOrigin)
|
|
908
1046
|
nIdx.set(this.indices)
|
|
1047
|
+
nCentroids.set(this.quadCentroids)
|
|
1048
|
+
nTemplate.set(this.quadIndexTemplate)
|
|
909
1049
|
|
|
910
1050
|
this.positions = nPos
|
|
911
1051
|
this.colors = nCol
|
|
@@ -914,6 +1054,8 @@ export class GlobalLegacyBuffer {
|
|
|
914
1054
|
this.uvs = nUv
|
|
915
1055
|
this.aOrigin = nOrigin
|
|
916
1056
|
this.indices = nIdx
|
|
1057
|
+
this.quadCentroids = nCentroids
|
|
1058
|
+
this.quadIndexTemplate = nTemplate
|
|
917
1059
|
this.capacityQuads = newCap
|
|
918
1060
|
|
|
919
1061
|
const geometry = this.mesh.geometry
|
|
@@ -938,5 +1080,6 @@ export class GlobalLegacyBuffer {
|
|
|
938
1080
|
geometry.setIndex(indexAttr)
|
|
939
1081
|
|
|
940
1082
|
this.pendingRanges.length = 0
|
|
1083
|
+
this.indexPendingRanges.length = 0
|
|
941
1084
|
}
|
|
942
1085
|
}
|
|
@@ -228,7 +228,7 @@ export function createGlobalLegacyBlendMaterial(): THREE.ShaderMaterial {
|
|
|
228
228
|
fragmentShader,
|
|
229
229
|
uniforms: THREE.UniformsUtils.merge([THREE.UniformsLib.fog, legacyUniforms]),
|
|
230
230
|
transparent: true,
|
|
231
|
-
depthWrite:
|
|
231
|
+
depthWrite: false,
|
|
232
232
|
depthTest: true,
|
|
233
233
|
vertexColors: true,
|
|
234
234
|
glslVersion: THREE.GLSL3,
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
import { describe, expect, test } from 'vitest'
|
|
3
3
|
import { buildVisibleCubeSpans } from '../cubeDrawSpans'
|
|
4
4
|
|
|
5
|
+
type DrawSpan = { start: number; count: number }
|
|
6
|
+
|
|
7
|
+
/** Every index in output spans must lie in the union of input section ranges. */
|
|
8
|
+
function expectEveryIndexInUnion(spans: DrawSpan[], sectionRanges: DrawSpan[]): void {
|
|
9
|
+
for (const span of spans) {
|
|
10
|
+
for (let i = span.start; i < span.start + span.count; i++) {
|
|
11
|
+
const inUnion = sectionRanges.some(r => i >= r.start && i < r.start + r.count)
|
|
12
|
+
expect(inUnion, `index ${i} not in union of section ranges`).toBe(true)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
describe('buildVisibleCubeSpans', () => {
|
|
6
18
|
test('contiguous slots merge into one span', () => {
|
|
7
19
|
const spans = buildVisibleCubeSpans(
|
|
@@ -100,4 +112,23 @@ describe('buildVisibleCubeSpans', () => {
|
|
|
100
112
|
expect(buildVisibleCubeSpans([], 10)).toEqual([])
|
|
101
113
|
expect(buildVisibleCubeSpans([{ start: 0, count: 1 }], 0)).toEqual([])
|
|
102
114
|
})
|
|
115
|
+
|
|
116
|
+
test('merged adjacent sections stay within union of input ranges', () => {
|
|
117
|
+
const sectionRanges = [
|
|
118
|
+
{ start: 100, count: 10 },
|
|
119
|
+
{ start: 110, count: 10 }
|
|
120
|
+
]
|
|
121
|
+
const spans = buildVisibleCubeSpans(sectionRanges, 120, false)
|
|
122
|
+
expect(spans).toEqual([{ start: 100, count: 20 }])
|
|
123
|
+
expectEveryIndexInUnion(spans, sectionRanges)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('carved merged spans stay within union of input ranges', () => {
|
|
127
|
+
const sectionRanges = [
|
|
128
|
+
{ start: 0, count: 10 },
|
|
129
|
+
{ start: 10, count: 10 }
|
|
130
|
+
]
|
|
131
|
+
const spans = buildVisibleCubeSpans(sectionRanges, 20, false, undefined, [{ start: 5, end: 14 }])
|
|
132
|
+
expectEveryIndexInUnion(spans, sectionRanges)
|
|
133
|
+
})
|
|
103
134
|
})
|