minecraft-renderer 0.1.77 → 0.1.79

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.
@@ -1,5 +1,14 @@
1
1
  //@ts-nocheck
2
2
  import * as THREE from 'three'
3
+ import {
4
+ createLegacyMultiDrawScratch,
5
+ detectLegacyMultiDrawCaps,
6
+ drawLegacySpans,
7
+ logLegacyMultiDrawTierOnce,
8
+ type LegacyDrawSpan,
9
+ type LegacyMultiDrawCaps,
10
+ type LegacyMultiDrawScratch,
11
+ } from './legacyMultiDraw'
3
12
  import { computeCameraRelativeUniforms, type RenderOrigin } from './shaders/legacyBlockShader'
4
13
 
5
14
  const VERTS_PER_QUAD = 4
@@ -11,6 +20,9 @@ const FLOATS_PER_LIGHT_VERT = 1
11
20
  const DEFAULT_INITIAL_CAPACITY_QUADS = 128_000
12
21
  const DEFAULT_GROWTH_INCREMENT_QUADS = 128_000
13
22
  const MAX_UPLOAD_QUADS_PER_FRAME = 5_000
23
+ const FRAGMENTATION_THRESHOLD = 0.25
24
+
25
+ type PendingMove = { key: string, oldStart: number, newStart: number, count: number }
14
26
 
15
27
  /** CPU bytes per allocated quad slot (all legacy vertex/index attrs). */
16
28
  export const LEGACY_BYTES_PER_QUAD =
@@ -44,6 +56,8 @@ export type LegacySectionGeometryData = LegacySectionGeometry & {
44
56
  sz: number
45
57
  }
46
58
 
59
+ export type { LegacyDrawSpan } from './legacyMultiDraw'
60
+
47
61
  /**
48
62
  * Single GPU mesh for legacy quads (opaque+cutout or transparent blend).
49
63
  * Camera-relative via per-vertex a_origin (relative to render origin) + u_originDelta uniforms.
@@ -67,6 +81,12 @@ export class GlobalLegacyBuffer {
67
81
  private pendingRanges: Array<{ start: number, end: number }> = []
68
82
  private readonly _spanScratch: Array<{ start: number, count: number }> = []
69
83
  private renderOrigin: RenderOrigin = { x: 0, y: 0, z: 0 }
84
+ private layoutVersion = 0
85
+ private pendingMove: PendingMove | null = null
86
+ private visibleIndexSpans: LegacyDrawSpan[] = []
87
+ private readonly _drawScratch: LegacyMultiDrawScratch = createLegacyMultiDrawScratch()
88
+ private multiDrawCaps: LegacyMultiDrawCaps | null = null
89
+ private debugOverlay = false
70
90
 
71
91
  constructor (
72
92
  material: THREE.ShaderMaterial,
@@ -114,6 +134,38 @@ export class GlobalLegacyBuffer {
114
134
  this.mesh.position.set(0, 0, 0)
115
135
  scene.add(this.mesh)
116
136
  this.syncDefaultDrawGroups()
137
+
138
+ this.mesh.onAfterRender = (renderer, _scene, _camera, _geometry, material) => {
139
+ if (this.visibleIndexSpans.length === 0) return
140
+ const gl = renderer.getContext() as WebGL2RenderingContext
141
+ if (!this.multiDrawCaps) {
142
+ this.multiDrawCaps = detectLegacyMultiDrawCaps(gl)
143
+ logLegacyMultiDrawTierOnce(this.multiDrawCaps.tier, this.debugOverlay)
144
+ }
145
+ drawLegacySpans(gl, this.multiDrawCaps, this.visibleIndexSpans, this._drawScratch)
146
+ }
147
+ }
148
+
149
+ setDebugOverlay (enabled: boolean): void {
150
+ this.debugOverlay = enabled
151
+ }
152
+
153
+ /**
154
+ * Suppress three's full-buffer indexed draw; onAfterRender issues visible spans only.
155
+ * setDrawRange(0,0) skips bindingStates.setup — use a minimal non-zero range so
156
+ * program/VAO/ELEMENT_ARRAY_BUFFER stay bound while three draws ~nothing.
157
+ * Draws one triangle from index 0 (usually harmless; if quad 0 is culled, one stray tri).
158
+ */
159
+ suppressThreeDraw (): void {
160
+ this.mesh.geometry.setDrawRange(0, 3)
161
+ }
162
+
163
+ setVisibleIndexSpans (spans: LegacyDrawSpan[]): void {
164
+ this.visibleIndexSpans = spans
165
+ }
166
+
167
+ getVisibleIndexSpans (): readonly LegacyDrawSpan[] {
168
+ return this.visibleIndexSpans
117
169
  }
118
170
 
119
171
  private syncDefaultDrawGroups (): void {
@@ -171,14 +223,14 @@ export class GlobalLegacyBuffer {
171
223
  this.uvs.set(geo.uvs, dstUvBase)
172
224
 
173
225
  const originOff = dstFloatBase
174
- const rx = this.renderOrigin.x
175
- const ry = this.renderOrigin.y
176
- const rz = this.renderOrigin.z
226
+ const ox = sx - this.renderOrigin.x
227
+ const oy = sy - this.renderOrigin.y
228
+ const oz = sz - this.renderOrigin.z
177
229
  for (let v = 0; v < vertCount; v++) {
178
230
  const o = originOff + v * FLOATS_PER_VERT
179
- this.aOrigin[o] = sx - rx
180
- this.aOrigin[o + 1] = sy - ry
181
- this.aOrigin[o + 2] = sz - rz
231
+ this.aOrigin[o] = ox
232
+ this.aOrigin[o + 1] = oy
233
+ this.aOrigin[o + 2] = oz
182
234
  }
183
235
 
184
236
  const dstIndexBase = slot.start * INDICES_PER_QUAD
@@ -190,15 +242,60 @@ export class GlobalLegacyBuffer {
190
242
  this.sectionSlots.set(sectionKey, slot)
191
243
  this.markDirty(slot.start, slot.start + quadCount - 1)
192
244
  this.syncDefaultDrawGroups()
245
+ this.layoutVersion++
193
246
  return true
194
247
  }
195
248
 
249
+ getLayoutVersion (): number {
250
+ return this.layoutVersion
251
+ }
252
+
253
+ getSectionDrawStart (sectionKey: string): number | undefined {
254
+ const slot = this.sectionSlots.get(sectionKey)
255
+ if (!slot) return undefined
256
+ if (this.pendingMove?.key === sectionKey) return this.pendingMove.oldStart
257
+ return slot.start
258
+ }
259
+
260
+ getPendingMove (): PendingMove | null {
261
+ return this.pendingMove
262
+ }
263
+
264
+ /** One interior-hole move per frame when fragmentation exceeds threshold; deferred shrink. */
265
+ compactStep (): void {
266
+ if (this.pendingMove) {
267
+ const { newStart, count } = this.pendingMove
268
+ if (this.rangeFullyUploaded(newStart, newStart + count - 1)) {
269
+ this.finalizePendingMove()
270
+ }
271
+ return
272
+ }
273
+
274
+ if (this.highWatermark === 0) return
275
+ const interiorFree = this.interiorFreeQuads()
276
+ if (interiorFree / this.highWatermark <= FRAGMENTATION_THRESHOLD) return
277
+
278
+ const section = this.findMovableSection(MAX_UPLOAD_QUADS_PER_FRAME)
279
+ if (!section) return
280
+
281
+ const hole = this.findLowestInteriorHole(section.start, section.count)
282
+ if (!hole) return
283
+
284
+ const reserved = this.reserveFreeSlotAt(hole.index, section.count)
285
+ const oldStart = section.start
286
+ const newStart = reserved.start
287
+
288
+ this.copySectionRange(oldStart, newStart, section.count)
289
+ this.sectionSlots.set(section.key, { start: newStart, count: section.count })
290
+ this.markDirty(newStart, newStart + section.count - 1)
291
+ this.pendingMove = { key: section.key, oldStart, newStart, count: section.count }
292
+ this.layoutVersion++
293
+ }
294
+
196
295
  updateDrawSpans (visible: VisibleSectionSpan[], mode: 'opaque' | 'sortedBlend'): void {
197
- const geometry = this.mesh.geometry
198
- geometry.clearGroups()
296
+ this.visibleIndexSpans = []
199
297
 
200
298
  if (this.highWatermark === 0) {
201
- geometry.setDrawRange(0, 0)
202
299
  return
203
300
  }
204
301
 
@@ -207,21 +304,30 @@ export class GlobalLegacyBuffer {
207
304
  let visibleQuadCount = 0
208
305
 
209
306
  for (const entry of visible) {
307
+ const drawStart = this.getSectionDrawStart(entry.key)
210
308
  const slot = this.sectionSlots.get(entry.key)
211
- if (!slot) continue
212
- spans.push({ start: slot.start, count: slot.count })
309
+ if (drawStart === undefined || !slot) continue
310
+ spans.push({ start: drawStart, count: slot.count })
213
311
  visibleQuadCount += slot.count
214
312
  }
215
313
 
216
314
  if (spans.length === 0) {
217
- geometry.setDrawRange(0, 0)
218
315
  return
219
316
  }
220
317
 
318
+ const pushIndexSpan = (quadStart: number, quadCount: number): void => {
319
+ this.visibleIndexSpans.push({
320
+ indexStart: quadStart * INDICES_PER_QUAD,
321
+ indexCount: quadCount * INDICES_PER_QUAD,
322
+ })
323
+ }
324
+
221
325
  if (mode === 'opaque') {
222
326
  if (visibleQuadCount >= this.highWatermark * FULL_DRAW_VISIBLE_FRACTION) {
223
- geometry.addGroup(0, this.highWatermark * INDICES_PER_QUAD, 0)
224
- geometry.setDrawRange(0, this.highWatermark * INDICES_PER_QUAD)
327
+ this.visibleIndexSpans.push({
328
+ indexStart: 0,
329
+ indexCount: this.highWatermark * INDICES_PER_QUAD,
330
+ })
225
331
  return
226
332
  }
227
333
 
@@ -230,18 +336,17 @@ export class GlobalLegacyBuffer {
230
336
  this.capOpaqueSpans(spans)
231
337
 
232
338
  for (const span of spans) {
233
- geometry.addGroup(span.start * INDICES_PER_QUAD, span.count * INDICES_PER_QUAD, 0)
339
+ pushIndexSpan(span.start, span.count)
234
340
  }
235
341
  } else {
236
342
  visible.sort((a, b) => b.distSq - a.distSq)
237
343
  for (const entry of visible) {
344
+ const drawStart = this.getSectionDrawStart(entry.key)
238
345
  const slot = this.sectionSlots.get(entry.key)
239
- if (!slot) continue
240
- geometry.addGroup(slot.start * INDICES_PER_QUAD, slot.count * INDICES_PER_QUAD, 0)
346
+ if (drawStart === undefined || !slot) continue
347
+ pushIndexSpan(drawStart, slot.count)
241
348
  }
242
349
  }
243
-
244
- geometry.setDrawRange(0, this.highWatermark * INDICES_PER_QUAD)
245
350
  }
246
351
 
247
352
  private mergeOpaqueSpans (spans: Array<{ start: number, count: number }>): void {
@@ -328,6 +433,18 @@ export class GlobalLegacyBuffer {
328
433
  const slot = this.sectionSlots.get(sectionKey)
329
434
  if (!slot) return
330
435
 
436
+ if (this.pendingMove?.key === sectionKey) {
437
+ const { oldStart, count } = this.pendingMove
438
+ const oldIndexBase = oldStart * INDICES_PER_QUAD
439
+ const oldIndexLen = count * INDICES_PER_QUAD
440
+ for (let i = 0; i < oldIndexLen; i++) {
441
+ this.indices[oldIndexBase + i] = 0
442
+ }
443
+ this.markDirty(oldStart, oldStart + count - 1)
444
+ this.insertFreeSlot({ start: oldStart, count })
445
+ this.pendingMove = null
446
+ }
447
+
331
448
  const dstIndexBase = slot.start * INDICES_PER_QUAD
332
449
  const indexLen = slot.count * INDICES_PER_QUAD
333
450
  for (let i = 0; i < indexLen; i++) {
@@ -339,6 +456,7 @@ export class GlobalLegacyBuffer {
339
456
  this.insertFreeSlot(slot)
340
457
  this.shrinkHighWatermark()
341
458
  this.syncDefaultDrawGroups()
459
+ this.layoutVersion++
342
460
  }
343
461
 
344
462
  hasPendingUploads (): boolean {
@@ -505,6 +623,8 @@ export class GlobalLegacyBuffer {
505
623
  this.freeList.length = 0
506
624
  this.highWatermark = 0
507
625
  this.pendingRanges.length = 0
626
+ this.pendingMove = null
627
+ this.visibleIndexSpans = []
508
628
  this.syncDefaultDrawGroups()
509
629
  }
510
630
 
@@ -585,7 +705,125 @@ export class GlobalLegacyBuffer {
585
705
  }
586
706
  }
587
707
 
708
+ private interiorFreeQuads (): number {
709
+ let total = 0
710
+ for (const slot of this.freeList) {
711
+ if (slot.start < this.highWatermark) total += slot.count
712
+ }
713
+ return total
714
+ }
715
+
716
+ private findMovableSection (maxCount: number): { key: string, start: number, count: number } | undefined {
717
+ const sections: Array<{ key: string, start: number, count: number }> = []
718
+ for (const [key, slot] of this.sectionSlots) {
719
+ sections.push({ key, start: slot.start, count: slot.count })
720
+ }
721
+ if (sections.length === 0) return undefined
722
+
723
+ sections.sort((a, b) => {
724
+ if (b.start !== a.start) return b.start - a.start
725
+ return (b.start + b.count) - (a.start + a.count)
726
+ })
727
+
728
+ const tailmost = sections[0]!
729
+ if (tailmost.count <= maxCount && this.findLowestInteriorHole(tailmost.start, tailmost.count)) {
730
+ return tailmost
731
+ }
732
+
733
+ const candidates = sections
734
+ .filter(s => s.count <= maxCount)
735
+ .sort((a, b) => b.count - a.count)
736
+
737
+ for (const s of candidates) {
738
+ if (this.findLowestInteriorHole(s.start, s.count)) return s
739
+ }
740
+ return undefined
741
+ }
742
+
743
+ private findLowestInteriorHole (
744
+ sectionStart: number,
745
+ count: number,
746
+ ): { start: number, count: number, index: number } | undefined {
747
+ for (let i = 0; i < this.freeList.length; i++) {
748
+ const slot = this.freeList[i]!
749
+ if (slot.start < sectionStart && slot.count >= count) {
750
+ return { start: slot.start, count: slot.count, index: i }
751
+ }
752
+ }
753
+ return undefined
754
+ }
755
+
756
+ private reserveFreeSlotAt (index: number, count: number): { start: number, count: number } {
757
+ const slot = this.freeList[index]!
758
+ this.freeList.splice(index, 1)
759
+ if (slot.count === count) return { start: slot.start, count }
760
+ const used = { start: slot.start, count }
761
+ this.insertFreeSlot({ start: slot.start + count, count: slot.count - count })
762
+ return used
763
+ }
764
+
765
+ private copySectionRange (oldStart: number, newStart: number, quadCount: number): void {
766
+ const oldVertBase = oldStart * VERTS_PER_QUAD
767
+ const newVertBase = newStart * VERTS_PER_QUAD
768
+ const vertCount = quadCount * VERTS_PER_QUAD
769
+ const vertDelta = newVertBase - oldVertBase
770
+
771
+ const oldFloatBase = oldVertBase * FLOATS_PER_VERT
772
+ const newFloatBase = newVertBase * FLOATS_PER_VERT
773
+ const floatLen = vertCount * FLOATS_PER_VERT
774
+ this.positions.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
775
+ this.colors.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
776
+ this.aOrigin.copyWithin(newFloatBase, oldFloatBase, oldFloatBase + floatLen)
777
+
778
+ const oldLightBase = oldVertBase * FLOATS_PER_LIGHT_VERT
779
+ const newLightBase = newVertBase * FLOATS_PER_LIGHT_VERT
780
+ const lightLen = vertCount * FLOATS_PER_LIGHT_VERT
781
+ this.skyLights.copyWithin(newLightBase, oldLightBase, oldLightBase + lightLen)
782
+ this.blockLights.copyWithin(newLightBase, oldLightBase, oldLightBase + lightLen)
783
+
784
+ const oldUvBase = oldVertBase * FLOATS_PER_UV_VERT
785
+ const newUvBase = newVertBase * FLOATS_PER_UV_VERT
786
+ const uvLen = vertCount * FLOATS_PER_UV_VERT
787
+ this.uvs.copyWithin(newUvBase, oldUvBase, oldUvBase + uvLen)
788
+
789
+ const oldIndexBase = oldStart * INDICES_PER_QUAD
790
+ const newIndexBase = newStart * INDICES_PER_QUAD
791
+ const indexLen = quadCount * INDICES_PER_QUAD
792
+ for (let i = 0; i < indexLen; i++) {
793
+ this.indices[newIndexBase + i] = this.indices[oldIndexBase + i]! + vertDelta
794
+ }
795
+ }
796
+
797
+ private rangeFullyUploaded (start: number, end: number): boolean {
798
+ for (const r of this.pendingRanges) {
799
+ if (r.start <= end && r.end >= start) return false
800
+ }
801
+ return true
802
+ }
803
+
804
+ private finalizePendingMove (): void {
805
+ const move = this.pendingMove
806
+ if (!move) return
807
+
808
+ const { oldStart, count } = move
809
+ const oldIndexBase = oldStart * INDICES_PER_QUAD
810
+ const oldIndexLen = count * INDICES_PER_QUAD
811
+ for (let i = 0; i < oldIndexLen; i++) {
812
+ this.indices[oldIndexBase + i] = 0
813
+ }
814
+ this.insertFreeSlot({ start: oldStart, count })
815
+ this.shrinkHighWatermark()
816
+ if (oldStart < this.highWatermark) {
817
+ this.markDirty(oldStart, oldStart + count - 1)
818
+ }
819
+ this.syncDefaultDrawGroups()
820
+ this.pendingMove = null
821
+ this.layoutVersion++
822
+ }
823
+
588
824
  private growCapacity (minQuads: number): void {
825
+ if (this.pendingMove) this.finalizePendingMove()
826
+
589
827
  let newCap = this.capacityQuads
590
828
  while (newCap < minQuads) newCap += this.growthIncrementQuads
591
829
 
@@ -0,0 +1,99 @@
1
+ //@ts-nocheck
2
+ import { MAX_OPAQUE_SPANS } from './globalLegacyBuffer'
3
+
4
+ export type LegacyMultiDrawTier = 'A' | 'B' | 'C'
5
+
6
+ export type LegacyDrawSpan = { indexStart: number, indexCount: number }
7
+
8
+ export type LegacyMultiDrawCaps = {
9
+ tier: LegacyMultiDrawTier
10
+ ext: MultiDrawElementsExt | null
11
+ }
12
+
13
+ type MultiDrawElementsExt = {
14
+ multiDrawElementsWEBGL: (
15
+ mode: number,
16
+ counts: Int32Array,
17
+ countsOffset: number,
18
+ type: number,
19
+ offsets: Int32Array,
20
+ offsetsOffset: number,
21
+ drawCount: number,
22
+ ) => void
23
+ }
24
+
25
+ export type LegacyMultiDrawScratch = {
26
+ counts: Int32Array
27
+ offsets: Int32Array
28
+ }
29
+
30
+ export function createLegacyMultiDrawScratch (): LegacyMultiDrawScratch {
31
+ return {
32
+ counts: new Int32Array(MAX_OPAQUE_SPANS),
33
+ offsets: new Int32Array(MAX_OPAQUE_SPANS),
34
+ }
35
+ }
36
+
37
+ /** Grow scratch arrays when blend (uncapped) span count exceeds opaque cap. */
38
+ function ensureScratchCapacity (scratch: LegacyMultiDrawScratch, minLength: number): void {
39
+ if (scratch.counts.length >= minLength) return
40
+ let newLen = scratch.counts.length
41
+ while (newLen < minLength) newLen *= 2
42
+ scratch.counts = new Int32Array(newLen)
43
+ scratch.offsets = new Int32Array(newLen)
44
+ }
45
+
46
+ export function detectLegacyMultiDrawCaps (gl: WebGL2RenderingContext): LegacyMultiDrawCaps {
47
+ const ext = gl.getExtension('WEBGL_multi_draw')
48
+ if (ext) {
49
+ return { tier: 'A', ext: ext as MultiDrawElementsExt }
50
+ }
51
+ return { tier: 'B', ext: null }
52
+ }
53
+
54
+ let tierLogged = false
55
+
56
+ export function logLegacyMultiDrawTierOnce (tier: LegacyMultiDrawTier, debug: boolean): void {
57
+ if (tierLogged || !debug) return
58
+ tierLogged = true
59
+ console.info('[globalLegacyBuffer] legacy multi_draw tier', tier)
60
+ }
61
+
62
+ /**
63
+ * Issue indexed legacy draws for visible spans. Tier B/C loop drawElements.
64
+ */
65
+ export function drawLegacySpans (
66
+ gl: WebGL2RenderingContext,
67
+ caps: LegacyMultiDrawCaps,
68
+ spans: readonly LegacyDrawSpan[],
69
+ scratch: LegacyMultiDrawScratch,
70
+ ): void {
71
+ const drawCount = spans.length
72
+ if (drawCount === 0) return
73
+
74
+ ensureScratchCapacity(scratch, drawCount)
75
+
76
+ const mode = gl.TRIANGLES
77
+ const type = gl.UNSIGNED_INT
78
+
79
+ for (let i = 0; i < drawCount; i++) {
80
+ const span = spans[i]!
81
+ scratch.counts[i] = span.indexCount
82
+ scratch.offsets[i] = span.indexStart * 4
83
+ }
84
+
85
+ if (caps.tier === 'A' && caps.ext) {
86
+ caps.ext.multiDrawElementsWEBGL(
87
+ mode,
88
+ scratch.counts, 0,
89
+ type,
90
+ scratch.offsets, 0,
91
+ drawCount,
92
+ )
93
+ return
94
+ }
95
+
96
+ for (let i = 0; i < drawCount; i++) {
97
+ gl.drawElements(mode, scratch.counts[i]!, type, scratch.offsets[i]!)
98
+ }
99
+ }
@@ -199,6 +199,8 @@ uniform float u_skyLevel;
199
199
  uniform float u_lightCurve;
200
200
  uniform float u_minBrightness;
201
201
  uniform float u_lightGamma;
202
+ uniform float u_shadingTheme; // 0 = vanilla, 1 = high-contrast
203
+ uniform float u_cardinalLight; // 0 = default, 1 = nether
202
204
 
203
205
  in float v_blockLight;
204
206
  in float v_skyLight;
@@ -248,6 +250,24 @@ void applyFog() {
248
250
 
249
251
  ${APPLY_LIGHTMAP_GLSL}
250
252
 
253
+ const vec3 FACE_NORMAL[6] = vec3[6](
254
+ vec3(0.0, 1.0, 0.0), // UP
255
+ vec3(0.0,-1.0, 0.0), // DOWN
256
+ vec3(1.0, 0.0, 0.0), // EAST
257
+ vec3(-1.0,0.0, 0.0), // WEST
258
+ vec3(0.0, 0.0, 1.0), // SOUTH
259
+ vec3(0.0, 0.0,-1.0) // NORTH
260
+ );
261
+
262
+ float sideShadingFromFaceId(int faceId, float theme, float cardinal) {
263
+ vec3 n = FACE_NORMAL[faceId];
264
+ float hc = 0.8 + 0.5 * max(0.0, 0.66*n.x + 0.66*n.y + 0.33*n.z);
265
+ float nether = 0.5 + abs(0.1*n.x + 0.4*n.y + 0.3*n.z);
266
+ float vanilla = 0.75 + 0.25*n.y + 0.05*(abs(n.z) - 3.0*abs(n.x));
267
+ float nonHc = mix(vanilla, nether, cardinal);
268
+ return mix(nonHc, hc, theme);
269
+ }
270
+
251
271
  void main() {
252
272
  // Atlas sample (pixelated, no filtering)
253
273
  ivec2 atlasSize = textureSize(u_atlas, 0);
@@ -294,7 +314,9 @@ void main() {
294
314
 
295
315
  float L = max(v_blockLight, min(v_skyLight, u_skyLevel));
296
316
  float Lm = applyLightmap(L);
297
- float brightness = Lm * v_ao;
317
+ float aoFactor = mix(0.8 * v_ao + 0.2, v_ao, u_shadingTheme);
318
+ float side = sideShadingFromFaceId(v_faceId, u_shadingTheme, u_cardinalLight);
319
+ float brightness = Lm * aoFactor * side;
298
320
 
299
321
  // Opaque full cubes: always alpha 1 (legacy uses cutout material; avoids seeing blocks behind)
300
322
  FragColor = vec4(baseColor.rgb * tint * brightness, 1.0);
@@ -317,6 +339,8 @@ export function createCubeBlockMaterial(): THREE.ShaderMaterial {
317
339
  u_tintPalette: { value: null },
318
340
  u_debugMode: { value: 0 },
319
341
  u_skyLevel: { value: 1.0 },
342
+ u_shadingTheme: { value: 1.0 },
343
+ u_cardinalLight: { value: 0.0 },
320
344
  u_lightCurve: { value: DEFAULT_LIGHTMAP_PARAMS.curve },
321
345
  u_minBrightness: { value: DEFAULT_LIGHTMAP_PARAMS.minBrightness },
322
346
  u_lightGamma: { value: DEFAULT_LIGHTMAP_PARAMS.gamma },
@@ -344,6 +368,17 @@ export function setCubeSkyLevel (material: THREE.ShaderMaterial, value: number):
344
368
  if (u) u.value = value
345
369
  }
346
370
 
371
+ export function setCubeShadingTheme (
372
+ material: THREE.ShaderMaterial,
373
+ theme: 'vanilla' | 'high-contrast',
374
+ cardinalLight: string,
375
+ ): void {
376
+ const t = material.uniforms.u_shadingTheme
377
+ if (t) t.value = theme === 'high-contrast' ? 1.0 : 0.0
378
+ const c = material.uniforms.u_cardinalLight
379
+ if (c) c.value = cardinalLight === 'nether' ? 1.0 : 0.0
380
+ }
381
+
347
382
  export function setCubeLightmapParams (
348
383
  material: THREE.ShaderMaterial,
349
384
  params: BlockLightmapParams,