minecraft-renderer 0.1.86 → 0.1.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecraft-renderer",
3
- "version": "0.1.86",
3
+ "version": "0.1.88",
4
4
  "description": "The most Modular Minecraft world renderer with Three.js WebGL backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
@@ -147,27 +147,27 @@ export class WalkingGeneralSwing extends PlayerAnimation {
147
147
  }
148
148
 
149
149
  if (this.isRunning) {
150
- player.skin.leftLeg.rotation.x = Math.cos(t + Math.PI) * 1.3
151
- player.skin.rightLeg.rotation.x = Math.cos(t) * 1.3
150
+ player.skin.leftLeg.rotation.x += Math.cos(t + Math.PI) * 1.3 * this._moveBlend
151
+ player.skin.rightLeg.rotation.x += Math.cos(t) * 1.3 * this._moveBlend
152
152
  } else {
153
- player.skin.leftLeg.rotation.x = Math.sin(t) * 0.5
154
- player.skin.rightLeg.rotation.x = Math.sin(t + Math.PI) * 0.5
153
+ player.skin.leftLeg.rotation.x += Math.sin(t) * 0.5 * this._moveBlend
154
+ player.skin.rightLeg.rotation.x += Math.sin(t + Math.PI) * 0.5 * this._moveBlend
155
155
  }
156
156
 
157
157
  if (this.isRunning) {
158
- player.skin.leftArm.rotation.x = Math.cos(t) * 1.5
159
- player.skin.rightArm.rotation.x = Math.cos(t + Math.PI) * 1.5
158
+ player.skin.leftArm.rotation.x += Math.cos(t) * 1.5 * this._moveBlend
159
+ player.skin.rightArm.rotation.x += Math.cos(t + Math.PI) * 1.5 * this._moveBlend
160
160
 
161
161
  const basicArmRotationZ = Math.PI * 0.1
162
- player.skin.leftArm.rotation.z = Math.cos(t) * 0.1 + basicArmRotationZ
163
- player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ
162
+ player.skin.leftArm.rotation.z += (Math.cos(t) * 0.1 + basicArmRotationZ) * this._moveBlend
163
+ player.skin.rightArm.rotation.z += (Math.cos(t + Math.PI) * 0.1 - basicArmRotationZ) * this._moveBlend
164
164
  } else {
165
- player.skin.leftArm.rotation.x = Math.sin(t + Math.PI) * 0.5
166
- player.skin.rightArm.rotation.x = Math.sin(t) * 0.5
165
+ player.skin.leftArm.rotation.x += Math.sin(t + Math.PI) * 0.5 * this._moveBlend
166
+ player.skin.rightArm.rotation.x += Math.sin(t) * 0.5 * this._moveBlend
167
167
 
168
168
  const basicArmRotationZ = Math.PI * 0.02
169
- player.skin.leftArm.rotation.z = Math.cos(t) * 0.03 + basicArmRotationZ
170
- player.skin.rightArm.rotation.z = Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ
169
+ player.skin.leftArm.rotation.z += (Math.cos(t) * 0.03 + basicArmRotationZ) * this._moveBlend
170
+ player.skin.rightArm.rotation.z += (Math.cos(t + Math.PI) * 0.03 - basicArmRotationZ) * this._moveBlend
171
171
  }
172
172
 
173
173
  if (this._swingTime !== null) {
@@ -178,15 +178,15 @@ export class WalkingGeneralSwing extends PlayerAnimation {
178
178
  }
179
179
 
180
180
  if (this.isRunning) {
181
- player.rotation.z = Math.cos(t + Math.PI) * 0.01
181
+ player.rotation.z = Math.cos(t + Math.PI) * 0.01 * this._moveBlend
182
182
  }
183
183
 
184
184
  if (this.isRunning) {
185
185
  const basicCapeRotationX = Math.PI * 0.3
186
- player.cape.rotation.x = Math.sin(t * 2) * 0.1 + basicCapeRotationX
186
+ player.cape.rotation.x += (Math.sin(t * 2) * 0.1 + basicCapeRotationX) * this._moveBlend
187
187
  } else {
188
188
  const basicCapeRotationX = Math.PI * 0.06
189
- player.cape.rotation.x = Math.sin(t / 1.5) * 0.06 + basicCapeRotationX
189
+ player.cape.rotation.x += (Math.sin(t / 1.5) * 0.06 + basicCapeRotationX) * this._moveBlend
190
190
  }
191
191
 
192
192
  if (reset) {
@@ -96,6 +96,8 @@ export type GlobalLegacyBufferOptions = {
96
96
  name?: string
97
97
  initialCapacityQuads?: number
98
98
  growthIncrementQuads?: number
99
+ /** When true, pre-sort blend indices on addSection and support per-quad reorder. */
100
+ sortBlend?: boolean
99
101
  }
100
102
 
101
103
  export type VisibleSectionSpan = { key: string; distSq: number }
@@ -126,6 +128,7 @@ export class GlobalLegacyBuffer {
126
128
  readonly material: THREE.ShaderMaterial
127
129
 
128
130
  private readonly growthIncrementQuads: number
131
+ private readonly blendSort: boolean
129
132
  private capacityQuads: number
130
133
  private positions: Float32Array
131
134
  private colors: Float32Array
@@ -134,10 +137,15 @@ export class GlobalLegacyBuffer {
134
137
  private uvs: Float32Array
135
138
  private aOrigin: Float32Array
136
139
  private indices: Uint32Array
140
+ /** Section-relative world centroid per physical quad (3 floats each). */
141
+ private quadCentroids: Float32Array
142
+ /** Per-quad local index template (6 bytes each, values 0..3). */
143
+ private quadIndexTemplate: Uint8Array
137
144
  private readonly sectionSlots = new Map<string, { start: number; count: number }>()
138
145
  private freeList: Array<{ start: number; count: number }> = []
139
146
  private highWatermark = 0
140
147
  private pendingRanges: Array<{ start: number; end: number }> = []
148
+ private indexPendingRanges: Array<{ start: number; end: number }> = []
141
149
  private readonly _spanScratch: Array<{ start: number; count: number }> = []
142
150
  private renderOrigin: RenderOrigin = { x: 0, y: 0, z: 0 }
143
151
  private layoutVersion = 0
@@ -146,12 +154,20 @@ export class GlobalLegacyBuffer {
146
154
  private uploadEpoch = 0
147
155
  private visibleIndexSpans: LegacyDrawSpan[] = []
148
156
  private readonly _drawScratch: LegacyMultiDrawScratch = createLegacyMultiDrawScratch()
157
+ /** Reusable scratch for applyBlendSort; grown on demand, never allocated per call. */
158
+ private _sortOrder: Uint32Array = new Uint32Array(0)
159
+ private _sortDistSq: Float64Array = new Float64Array(0)
149
160
  private multiDrawCaps: LegacyMultiDrawCaps | null = null
150
161
  private debugOverlay = false
162
+ private _camX = 0
163
+ private _camY = 0
164
+ private _camZ = 0
165
+ private _camInitialized = false
151
166
 
152
167
  constructor(material: THREE.ShaderMaterial, scene: THREE.Object3D, opts?: GlobalLegacyBufferOptions) {
153
168
  this.material = material
154
169
  this.growthIncrementQuads = opts?.growthIncrementQuads ?? DEFAULT_GROWTH_INCREMENT_QUADS
170
+ this.blendSort = opts?.sortBlend ?? false
155
171
  this.capacityQuads = opts?.initialCapacityQuads ?? DEFAULT_INITIAL_CAPACITY_QUADS
156
172
  const maxVerts = this.capacityQuads * VERTS_PER_QUAD
157
173
  this.positions = new Float32Array(maxVerts * FLOATS_PER_VERT)
@@ -161,6 +177,8 @@ export class GlobalLegacyBuffer {
161
177
  this.uvs = new Float32Array(maxVerts * FLOATS_PER_UV_VERT)
162
178
  this.aOrigin = new Float32Array(maxVerts * FLOATS_PER_VERT)
163
179
  this.indices = new Uint32Array(this.capacityQuads * INDICES_PER_QUAD)
180
+ this.quadCentroids = new Float32Array(this.capacityQuads * 3)
181
+ this.quadIndexTemplate = new Uint8Array(this.capacityQuads * INDICES_PER_QUAD)
164
182
 
165
183
  const geometry = new THREE.BufferGeometry()
166
184
  const mkAttr = (arr: Float32Array, itemSize: number, name: string) => {
@@ -305,10 +323,38 @@ export class GlobalLegacyBuffer {
305
323
  this.indices[dstIndexBase + i] = geo.indices[i]! + vertexBase
306
324
  }
307
325
 
326
+ for (let q = 0; q < quadCount; q++) {
327
+ const physQuad = slot.start + q
328
+ const localVertBase = q * VERTS_PER_QUAD
329
+ const posBase = localVertBase * FLOATS_PER_VERT
330
+ let cx = 0
331
+ let cy = 0
332
+ let cz = 0
333
+ for (let v = 0; v < VERTS_PER_QUAD; v++) {
334
+ const p = posBase + v * FLOATS_PER_VERT
335
+ cx += geo.positions[p]!
336
+ cy += geo.positions[p + 1]!
337
+ cz += geo.positions[p + 2]!
338
+ }
339
+ const centBase = physQuad * 3
340
+ this.quadCentroids[centBase] = cx / VERTS_PER_QUAD
341
+ this.quadCentroids[centBase + 1] = cy / VERTS_PER_QUAD
342
+ this.quadCentroids[centBase + 2] = cz / VERTS_PER_QUAD
343
+
344
+ const idxBase = q * INDICES_PER_QUAD
345
+ const tmplBase = physQuad * INDICES_PER_QUAD
346
+ for (let i = 0; i < INDICES_PER_QUAD; i++) {
347
+ this.quadIndexTemplate[tmplBase + i] = geo.indices[idxBase + i]! - localVertBase
348
+ }
349
+ }
350
+
308
351
  this.sectionSlots.set(sectionKey, slot)
309
352
  if (isRemesh && previousSlot) {
310
353
  this.pendingReplace.set(sectionKey, { oldStart: previousSlot.start, oldCount: previousSlot.count })
311
354
  }
355
+ if (this.blendSort && this._camInitialized && quadCount >= 2) {
356
+ this.applyBlendSort(slot.start, quadCount, this._camX, this._camY, this._camZ)
357
+ }
312
358
  this.markDirty(slot.start, slot.start + quadCount - 1)
313
359
  this.syncDefaultDrawGroups()
314
360
  this.layoutVersion++
@@ -558,6 +604,70 @@ export class GlobalLegacyBuffer {
558
604
  return this.pendingRanges.length > 0
559
605
  }
560
606
 
607
+ hasPendingIndexUploads(): boolean {
608
+ return this.indexPendingRanges.length > 0
609
+ }
610
+
611
+ /**
612
+ * Reorder a section's index buffer back-to-front by quad centroid distance to camera.
613
+ * Does not bump layoutVersion or uploadEpoch (draw spans unchanged).
614
+ */
615
+ reorderSectionBlendIndices(sectionKey: string, camX: number, camY: number, camZ: number): boolean {
616
+ const slot = this.sectionSlots.get(sectionKey)
617
+ if (!slot) return false
618
+ if (this.pendingMove?.key === sectionKey) return false
619
+ if (this.pendingReplace.has(sectionKey)) return false
620
+ if (!this.rangeFullyUploaded(slot.start, slot.start + slot.count - 1)) return false
621
+ if (slot.count < 2) return false
622
+
623
+ this.applyBlendSort(slot.start, slot.count, camX, camY, camZ)
624
+ this.markIndexDirty(slot.start, slot.start + slot.count - 1)
625
+ return true
626
+ }
627
+
628
+ /** Rewrite indices for [slotStart, slotStart+slotCount) back-to-front by quad centroid distance. */
629
+ private applyBlendSort(slotStart: number, slotCount: number, camX: number, camY: number, camZ: number): void {
630
+ const dstFloatBase = slotStart * VERTS_PER_QUAD * FLOATS_PER_VERT
631
+ const sx = this.aOrigin[dstFloatBase]! + this.renderOrigin.x
632
+ const sy = this.aOrigin[dstFloatBase + 1]! + this.renderOrigin.y
633
+ const sz = this.aOrigin[dstFloatBase + 2]! + this.renderOrigin.z
634
+
635
+ if (this._sortOrder.length < slotCount) {
636
+ this._sortOrder = new Uint32Array(slotCount)
637
+ this._sortDistSq = new Float64Array(slotCount)
638
+ }
639
+ const order = this._sortOrder
640
+ const distSq = this._sortDistSq
641
+ for (let p = 0; p < slotCount; p++) {
642
+ order[p] = p
643
+ const physQuad = slotStart + p
644
+ const centBase = physQuad * 3
645
+ const wx = sx + this.quadCentroids[centBase]!
646
+ const wy = sy + this.quadCentroids[centBase + 1]!
647
+ const wz = sz + this.quadCentroids[centBase + 2]!
648
+ const dx = wx - camX
649
+ const dy = wy - camY
650
+ const dz = wz - camZ
651
+ distSq[p] = dx * dx + dy * dy + dz * dz
652
+ }
653
+ const orderView = order.subarray(0, slotCount)
654
+ orderView.sort((a, b) => {
655
+ const d = distSq[b]! - distSq[a]!
656
+ if (d !== 0) return d
657
+ return a - b
658
+ })
659
+
660
+ for (let k = 0; k < slotCount; k++) {
661
+ const src = orderView[k]!
662
+ const srcVertBase = (slotStart + src) * VERTS_PER_QUAD
663
+ const dstIdxBase = (slotStart + k) * INDICES_PER_QUAD
664
+ const tmplBase = (slotStart + src) * INDICES_PER_QUAD
665
+ for (let i = 0; i < INDICES_PER_QUAD; i++) {
666
+ this.indices[dstIdxBase + i] = srcVertBase + this.quadIndexTemplate[tmplBase + i]!
667
+ }
668
+ }
669
+ }
670
+
561
671
  uploadDirtyRange(): void {
562
672
  const r = this.pendingRanges[0]
563
673
  if (!r) return
@@ -613,6 +723,27 @@ export class GlobalLegacyBuffer {
613
723
  this.uploadEpoch++
614
724
  }
615
725
 
726
+ uploadDirtyIndexRange(): void {
727
+ const r = this.indexPendingRanges[0]
728
+ if (!r) return
729
+
730
+ const quadOffset = r.start
731
+ const quadCount = Math.min(r.end - r.start + 1, MAX_UPLOAD_QUADS_PER_FRAME)
732
+ const indexOffset = quadOffset * INDICES_PER_QUAD
733
+ const indexCount = quadCount * INDICES_PER_QUAD
734
+
735
+ const indexAttr = this.mesh.geometry.index as THREE.BufferAttribute
736
+ indexAttr.clearUpdateRanges()
737
+ indexAttr.addUpdateRange(indexOffset, indexCount)
738
+ indexAttr.needsUpdate = true
739
+
740
+ if (quadOffset + quadCount > r.end) {
741
+ this.indexPendingRanges.shift()
742
+ } else {
743
+ r.start = quadOffset + quadCount
744
+ }
745
+ }
746
+
616
747
  setRenderOrigin(renderOrigin: RenderOrigin): void {
617
748
  this.renderOrigin = { ...renderOrigin }
618
749
  }
@@ -637,6 +768,10 @@ export class GlobalLegacyBuffer {
637
768
  }
638
769
 
639
770
  setCameraOrigin(x: number, y: number, z: number): void {
771
+ this._camX = x
772
+ this._camY = y
773
+ this._camZ = z
774
+ this._camInitialized = true
640
775
  const { originDelta, cameraOriginFrac } = computeCameraRelativeUniforms(this.renderOrigin, x, y, z)
641
776
  const u = this.material.uniforms.u_originDelta
642
777
  if (u?.value?.set) u.value.set(originDelta.x, originDelta.y, originDelta.z)
@@ -669,6 +804,7 @@ export class GlobalLegacyBuffer {
669
804
  this.freeList.length = 0
670
805
  this.highWatermark = 0
671
806
  this.pendingRanges.length = 0
807
+ this.indexPendingRanges.length = 0
672
808
  this.pendingMove = null
673
809
  this.pendingReplace.clear()
674
810
  this.uploadEpoch = 0
@@ -688,6 +824,29 @@ export class GlobalLegacyBuffer {
688
824
  this.mergePendingRanges()
689
825
  }
690
826
 
827
+ private markIndexDirty(start: number, end: number): void {
828
+ this.indexPendingRanges.push({ start, end })
829
+ this.indexPendingRanges.sort((a, b) => a.start - b.start)
830
+ this.mergeIndexPendingRanges()
831
+ }
832
+
833
+ private mergeIndexPendingRanges(): void {
834
+ if (this.indexPendingRanges.length < 2) return
835
+ const merged: Array<{ start: number; end: number }> = []
836
+ let cur = this.indexPendingRanges[0]!
837
+ for (let i = 1; i < this.indexPendingRanges.length; i++) {
838
+ const next = this.indexPendingRanges[i]!
839
+ if (next.start <= cur.end + 1) {
840
+ cur = { start: cur.start, end: Math.max(cur.end, next.end) }
841
+ } else {
842
+ merged.push(cur)
843
+ cur = next
844
+ }
845
+ }
846
+ merged.push(cur)
847
+ this.indexPendingRanges = merged
848
+ }
849
+
691
850
  private mergePendingRanges(): void {
692
851
  if (this.pendingRanges.length < 2) return
693
852
  const merged: Array<{ start: number; end: number }> = []
@@ -835,6 +994,14 @@ export class GlobalLegacyBuffer {
835
994
  for (let i = 0; i < indexLen; i++) {
836
995
  this.indices[newIndexBase + i] = this.indices[oldIndexBase + i]! + vertDelta
837
996
  }
997
+
998
+ const oldCentroidBase = oldStart * 3
999
+ const newCentroidBase = newStart * 3
1000
+ this.quadCentroids.copyWithin(newCentroidBase, oldCentroidBase, oldCentroidBase + quadCount * 3)
1001
+
1002
+ const oldTemplateBase = oldStart * INDICES_PER_QUAD
1003
+ const newTemplateBase = newStart * INDICES_PER_QUAD
1004
+ this.quadIndexTemplate.copyWithin(newTemplateBase, oldTemplateBase, oldTemplateBase + quadCount * INDICES_PER_QUAD)
838
1005
  }
839
1006
 
840
1007
  private rangeFullyUploaded(start: number, end: number): boolean {
@@ -898,6 +1065,8 @@ export class GlobalLegacyBuffer {
898
1065
  const nUv = new Float32Array(newMaxVerts * FLOATS_PER_UV_VERT)
899
1066
  const nOrigin = new Float32Array(newMaxVerts * FLOATS_PER_VERT)
900
1067
  const nIdx = new Uint32Array(newCap * INDICES_PER_QUAD)
1068
+ const nCentroids = new Float32Array(newCap * 3)
1069
+ const nTemplate = new Uint8Array(newCap * INDICES_PER_QUAD)
901
1070
 
902
1071
  nPos.set(this.positions)
903
1072
  nCol.set(this.colors)
@@ -906,6 +1075,8 @@ export class GlobalLegacyBuffer {
906
1075
  nUv.set(this.uvs)
907
1076
  nOrigin.set(this.aOrigin)
908
1077
  nIdx.set(this.indices)
1078
+ nCentroids.set(this.quadCentroids)
1079
+ nTemplate.set(this.quadIndexTemplate)
909
1080
 
910
1081
  this.positions = nPos
911
1082
  this.colors = nCol
@@ -914,6 +1085,8 @@ export class GlobalLegacyBuffer {
914
1085
  this.uvs = nUv
915
1086
  this.aOrigin = nOrigin
916
1087
  this.indices = nIdx
1088
+ this.quadCentroids = nCentroids
1089
+ this.quadIndexTemplate = nTemplate
917
1090
  this.capacityQuads = newCap
918
1091
 
919
1092
  const geometry = this.mesh.geometry
@@ -938,5 +1111,6 @@ export class GlobalLegacyBuffer {
938
1111
  geometry.setIndex(indexAttr)
939
1112
 
940
1113
  this.pendingRanges.length = 0
1114
+ this.indexPendingRanges.length = 0
941
1115
  }
942
1116
  }
@@ -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: true,
231
+ depthWrite: false,
232
232
  depthTest: true,
233
233
  vertexColors: true,
234
234
  glslVersion: THREE.GLSL3,
@@ -22,8 +22,30 @@ function makeQuadGeometry(): {
22
22
  }
23
23
  }
24
24
 
25
+ /** Two quads along +Z: quad 0 centered at z=0, quad 1 centered at z=8. */
26
+ function makeTwoQuadGeometry(): {
27
+ positions: Float32Array
28
+ colors: Float32Array
29
+ skyLights: Float32Array
30
+ blockLights: Float32Array
31
+ uvs: Float32Array
32
+ indices: Uint32Array
33
+ } {
34
+ return {
35
+ positions: new Float32Array([-1, 0, -1, 1, 0, -1, 1, 0, 1, -1, 0, 1, -1, 0, 7, 1, 0, 7, 1, 0, 9, -1, 0, 9]),
36
+ colors: new Float32Array(24).fill(1),
37
+ skyLights: new Float32Array(8).fill(1),
38
+ blockLights: new Float32Array(8).fill(0),
39
+ uvs: new Float32Array(16).fill(0),
40
+ indices: new Uint32Array([0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 5, 6])
41
+ }
42
+ }
43
+
25
44
  type BufferInternals = {
26
45
  pendingRanges: Array<{ start: number; end: number }>
46
+ indexPendingRanges: Array<{ start: number; end: number }>
47
+ quadCentroids: Float32Array
48
+ quadIndexTemplate: Uint8Array
27
49
  pendingMove: { key: string; oldStart: number; newStart: number; count: number } | null
28
50
  growCapacity: (minQuads: number) => void
29
51
  }
@@ -36,6 +58,10 @@ function drainUploads(buffer: GlobalLegacyBuffer): void {
36
58
  while (getInternals(buffer).pendingRanges.length) buffer.uploadDirtyRange()
37
59
  }
38
60
 
61
+ function drainIndexUploads(buffer: GlobalLegacyBuffer): void {
62
+ while (buffer.hasPendingIndexUploads()) buffer.uploadDirtyIndexRange()
63
+ }
64
+
39
65
  function finishCurrentMove(buffer: GlobalLegacyBuffer): void {
40
66
  drainUploads(buffer)
41
67
  buffer.compactStep()
@@ -760,3 +786,94 @@ test('GlobalLegacyBuffer: suppressThreeDraw uses minimal non-zero draw range', (
760
786
  buffer.dispose()
761
787
  mat.dispose()
762
788
  })
789
+
790
+ test('GlobalLegacyBuffer: reorderSectionBlendIndices sorts quads back-to-front', () => {
791
+ const scene = new THREE.Scene()
792
+ const mat = createGlobalLegacyBlockMaterial()
793
+ const buffer = new GlobalLegacyBuffer(mat, scene)
794
+ const geo = makeTwoQuadGeometry()
795
+
796
+ buffer.addSection('blend', geo, 0, 0, 0)
797
+ drainUploads(buffer)
798
+
799
+ const layoutBefore = buffer.getLayoutVersion()
800
+ const slot = buffer.getSectionSlot('blend')!
801
+ const vertBase0 = slot.start * 4
802
+ const vertBase1 = (slot.start + 1) * 4
803
+
804
+ expect(buffer.reorderSectionBlendIndices('blend', 0, 0, -20)).toBe(true)
805
+ expect(buffer.getLayoutVersion()).toBe(layoutBefore)
806
+
807
+ let indices = readSectionIndices(buffer, 'blend')
808
+ expect(indices.slice(0, 6)).toEqual([vertBase1, vertBase1 + 2, vertBase1 + 1, vertBase1, vertBase1 + 1, vertBase1 + 2])
809
+ expect(indices.slice(6, 12)).toEqual([vertBase0, vertBase0 + 1, vertBase0 + 2, vertBase0, vertBase0 + 2, vertBase0 + 3])
810
+
811
+ expect(buffer.reorderSectionBlendIndices('blend', 0, 0, 20)).toBe(true)
812
+ indices = readSectionIndices(buffer, 'blend')
813
+ expect(indices.slice(0, 6)).toEqual([vertBase0, vertBase0 + 1, vertBase0 + 2, vertBase0, vertBase0 + 2, vertBase0 + 3])
814
+ expect(indices.slice(6, 12)).toEqual([vertBase1, vertBase1 + 2, vertBase1 + 1, vertBase1, vertBase1 + 1, vertBase1 + 2])
815
+
816
+ expect(getInternals(buffer).indexPendingRanges.length).toBeGreaterThan(0)
817
+ drainIndexUploads(buffer)
818
+ expect(buffer.hasPendingIndexUploads()).toBe(false)
819
+
820
+ buffer.dispose()
821
+ mat.dispose()
822
+ })
823
+
824
+ test('GlobalLegacyBuffer: sortBlend pre-sorts indices on addSection before upload', () => {
825
+ const scene = new THREE.Scene()
826
+ const mat = createGlobalLegacyBlockMaterial()
827
+ const buffer = new GlobalLegacyBuffer(mat, scene, { sortBlend: true })
828
+ const geo = makeTwoQuadGeometry()
829
+
830
+ buffer.setCameraOrigin(0, 0, -20)
831
+ buffer.addSection('blend', geo, 0, 0, 0)
832
+
833
+ const slot = buffer.getSectionSlot('blend')!
834
+ const vertBase0 = slot.start * 4
835
+ const vertBase1 = (slot.start + 1) * 4
836
+ const indices = readSectionIndices(buffer, 'blend')
837
+
838
+ expect(indices.slice(0, 6)).toEqual([vertBase1, vertBase1 + 2, vertBase1 + 1, vertBase1, vertBase1 + 1, vertBase1 + 2])
839
+ expect(indices.slice(6, 12)).toEqual([vertBase0, vertBase0 + 1, vertBase0 + 2, vertBase0, vertBase0 + 2, vertBase0 + 3])
840
+ expect(getInternals(buffer).indexPendingRanges.length).toBe(0)
841
+
842
+ buffer.dispose()
843
+ mat.dispose()
844
+ })
845
+
846
+ test('GlobalLegacyBuffer: blend reorder metadata survives compaction', () => {
847
+ const scene = new THREE.Scene()
848
+ const mat = createGlobalLegacyBlockMaterial()
849
+ const buffer = new GlobalLegacyBuffer(mat, scene)
850
+ const geo = makeQuadGeometry()
851
+ const twoGeo = makeTwoQuadGeometry()
852
+
853
+ buffer.addSection('a', geo, 0, 0, 0)
854
+ buffer.addSection('b', geo, 8, 0, 0)
855
+ buffer.addSection('blend', twoGeo, 16, 0, 0)
856
+ expect(buffer.getSectionSlot('blend')).toEqual({ start: 2, count: 2 })
857
+
858
+ buffer.removeSection('a')
859
+ buffer.removeSection('b')
860
+ drainUploads(buffer)
861
+ buffer.compactStep()
862
+ finishCurrentMove(buffer)
863
+
864
+ const slot = buffer.getSectionSlot('blend')!
865
+ expect(slot).toEqual({ start: 0, count: 2 })
866
+
867
+ const internals = getInternals(buffer)
868
+ expect(internals.quadCentroids[slot.start * 3 + 2]).toBeCloseTo(0, 5)
869
+ expect(internals.quadCentroids[(slot.start + 1) * 3 + 2]).toBeCloseTo(8, 5)
870
+ expect(Array.from(internals.quadIndexTemplate.slice((slot.start + 1) * 6, (slot.start + 1) * 6 + 6))).toEqual([0, 2, 1, 0, 1, 2])
871
+
872
+ expect(buffer.reorderSectionBlendIndices('blend', 0, 0, -20)).toBe(true)
873
+ const vertBase1 = (slot.start + 1) * 4
874
+ const indices = readSectionIndices(buffer, 'blend')
875
+ expect(indices[0]).toBe(vertBase1)
876
+
877
+ buffer.dispose()
878
+ mat.dispose()
879
+ })
@@ -1327,11 +1327,13 @@ export class WorldRendererThree extends WorldRendererCommon {
1327
1327
  globalLegacyBuffer.suppressThreeDraw()
1328
1328
  }
1329
1329
  const globalLegacyBlendBuffer = this.chunkMeshManager.globalLegacyBlendBuffer
1330
+ let didBlendAllAttrUpload = false
1330
1331
  if (globalLegacyBlendBuffer) {
1331
1332
  globalLegacyBlendBuffer.setDebugOverlay(this.displayOptions.inWorldRenderingConfig.enableDebugOverlay)
1332
1333
  globalLegacyBlendBuffer.compactStep()
1333
1334
  if (globalLegacyBlendBuffer.hasPendingUploads()) {
1334
1335
  globalLegacyBlendBuffer.uploadDirtyRange()
1336
+ didBlendAllAttrUpload = true
1335
1337
  }
1336
1338
  globalLegacyBlendBuffer.suppressThreeDraw()
1337
1339
  }
@@ -1342,6 +1344,10 @@ export class WorldRendererThree extends WorldRendererCommon {
1342
1344
  this.chunkMeshManager.updateSectionCullAndSort(cam, this.cameraWorldPos.x, this.cameraWorldPos.y, this.cameraWorldPos.z)
1343
1345
  this.chunkMeshManager.clearCullDirty()
1344
1346
  }
1347
+ this.chunkMeshManager.sortVisibleBlendSections(camX, camY, camZ)
1348
+ if (!didBlendAllAttrUpload && globalLegacyBlendBuffer?.hasPendingIndexUploads()) {
1349
+ globalLegacyBlendBuffer.uploadDirtyIndexRange()
1350
+ }
1345
1351
  this.renderer.render(this.scene, cam)
1346
1352
 
1347
1353
  if (