minecraft-renderer 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/mesher.js +1 -1
  2. package/dist/mesher.js.map +2 -2
  3. package/dist/mesherWasm.js +3740 -183
  4. package/dist/minecraft-renderer.js +332 -60
  5. package/dist/minecraft-renderer.js.meta.json +1 -1
  6. package/dist/threeWorker.js +705 -433
  7. package/package.json +1 -1
  8. package/src/graphicsBackend/config.ts +4 -0
  9. package/src/graphicsBackend/playerState.ts +1 -0
  10. package/src/graphicsBackend/rendererOptionsSync.ts +1 -0
  11. package/src/lib/worldrendererCommon.ts +13 -0
  12. package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
  13. package/src/mesher-shared/shared.ts +8 -0
  14. package/src/playerState/playerState.ts +2 -0
  15. package/src/three/chunkMeshManager.ts +312 -39
  16. package/src/three/globalBlockBuffer.ts +292 -0
  17. package/src/three/menuBackground/config.ts +1 -1
  18. package/src/three/menuBackground/defaultOptions.ts +27 -19
  19. package/src/three/modules/sciFiWorldReveal.ts +162 -68
  20. package/src/three/modules/starfield.ts +9 -1
  21. package/src/three/sectionRaycastAabb.ts +167 -0
  22. package/src/three/shaderCubeMesh.ts +93 -0
  23. package/src/three/shaders/cubeBlockShader.ts +354 -0
  24. package/src/three/shaders/textureIndexMapping.ts +122 -0
  25. package/src/three/shaders/tintPalette.ts +198 -0
  26. package/src/three/worldGeometryExport.ts +53 -25
  27. package/src/three/worldRendererThree.ts +93 -26
  28. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  29. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +396 -0
  30. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  31. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
  32. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
  33. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
  34. package/src/wasm-mesher/worker/mesherWasm.ts +17 -2
@@ -0,0 +1,354 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+
4
+ // Face order: UP=0, DOWN=1, EAST=2, WEST=3, SOUTH=4, NORTH=5
5
+ // matches WASM mesher face order (mesher.rs FACE_NAMES)
6
+
7
+ const vertexShader = /* glsl */ `
8
+ precision highp float;
9
+ precision highp int;
10
+
11
+ layout(location = 0) in uint a_w0;
12
+ layout(location = 1) in uint a_w1;
13
+ layout(location = 2) in uint a_w2;
14
+ layout(location = 3) in uint a_w3;
15
+
16
+ // World camera position split for stable float32 subtraction (see relativePos below).
17
+ uniform vec3 u_cameraOrigin;
18
+ uniform vec3 u_cameraOriginFrac;
19
+
20
+ out float v_light;
21
+ out float v_ao;
22
+ out vec2 v_uv;
23
+ flat out int v_texIndex;
24
+ flat out int v_tintIndex;
25
+ flat out int v_faceId;
26
+
27
+ // Logarithmic depth buffer support: Three.js injects USE_LOGDEPTHBUF when the
28
+ // renderer has logarithmicDepthBuffer: true. Standard Three.js shader chunks
29
+ // rewrite gl_FragDepth via these varyings — if we don't, our linear gl_FragCoord.z
30
+ // fails depth test vs sibling meshes that DO write log depth (we'd be invisible).
31
+ // The renderer always uses a perspective camera, so we skip the vIsPerspective
32
+ // varying and always emit log depth — avoids the smooth-interpolation precision
33
+ // issue where vIsPerspective lands on 0.9999… on some pixels and silently falls
34
+ // back to linear gl_FragCoord.z, producing a white-noise z-fight pattern against
35
+ // neighbouring meshes.
36
+ #ifdef USE_LOGDEPTHBUF
37
+ out float vFragDepth;
38
+ #endif
39
+
40
+ // Fog support: Three.js injects USE_FOG when scene.fog is set AND material.fog === true.
41
+ // Standard MeshBasicMaterial enables this by default; ShaderMaterial does NOT, which is
42
+ // why our blocks looked unaffected by distance haze while neighbouring legacy meshes
43
+ // faded into the fog. createCubeBlockMaterial sets fog: true and merges
44
+ // UniformsLib.fog so Three.js can auto-refresh the fog uniforms each frame.
45
+ #ifdef USE_FOG
46
+ out float vFogDepth;
47
+ #endif
48
+
49
+ // projectionMatrix / modelViewMatrix: injected by Three.js ShaderMaterial (do not redeclare)
50
+
51
+ // BASE, DU, DV per face (UP=0, DOWN=1, EAST=2, WEST=3, SOUTH=4, NORTH=5)
52
+ // position = BASE[faceId] + u * DU[faceId] + v * DV[faceId]
53
+ const vec3 BASE[6] = vec3[6](
54
+ vec3(0.0, 1.0, 1.0), // UP (+Y)
55
+ vec3(1.0, 0.0, 1.0), // DOWN (-Y)
56
+ vec3(1.0, 1.0, 1.0), // EAST (+X)
57
+ vec3(0.0, 1.0, 0.0), // WEST (-X)
58
+ vec3(0.0, 1.0, 1.0), // SOUTH (+Z)
59
+ vec3(1.0, 1.0, 0.0) // NORTH (-Z)
60
+ );
61
+
62
+ const vec3 DU[6] = vec3[6](
63
+ vec3( 1.0, 0.0, 0.0), // UP
64
+ vec3(-1.0, 0.0, 0.0), // DOWN
65
+ vec3( 0.0,-1.0, 0.0), // EAST
66
+ vec3( 0.0,-1.0, 0.0), // WEST
67
+ vec3( 1.0, 0.0, 0.0), // SOUTH
68
+ vec3(-1.0, 0.0, 0.0) // NORTH
69
+ );
70
+
71
+ const vec3 DV[6] = vec3[6](
72
+ vec3(0.0, 0.0, -1.0), // UP
73
+ vec3(0.0, 0.0, -1.0), // DOWN
74
+ vec3(0.0, 0.0, -1.0), // EAST
75
+ vec3(0.0, 0.0, 1.0), // WEST
76
+ vec3(0.0,-1.0, 0.0), // SOUTH
77
+ vec3(0.0,-1.0, 0.0) // NORTH
78
+ );
79
+
80
+ // Per-(triangle, corner) -> quad corner index (vi), one table per diagonal mode.
81
+ // Normal: T0=[0,1,2], T1=[2,1,3]. Flipped: T0=[0,3,2], T1=[0,1,3].
82
+ const int VI_NORMAL[6] = int[6](0, 1, 2, 2, 1, 3);
83
+ const int VI_FLIPPED[6] = int[6](0, 3, 2, 0, 1, 3);
84
+
85
+ void main() {
86
+ // Empty slot sentinel (freed section range in global buffer)
87
+ if (((a_w2 >> 18u) & 0x1u) != 0u) {
88
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
89
+ return;
90
+ }
91
+
92
+ // Non-indexed geometry: 6 vertices per face instance (2 triangles)
93
+ int vi_total = gl_VertexID % 6;
94
+ int triangle = vi_total / 3; // 0 or 1
95
+ int corner = vi_total - triangle * 3; // 0,1,2
96
+
97
+ uint faceId = (a_w0 >> 12u) & 0x7u;
98
+
99
+ // Diagonal flip flag from word2 bit 12
100
+ uint diagonalFlag = (a_w2 >> 12u) & 0x1u;
101
+
102
+ // SOUTH/NORTH (faceId 4/5) need reversed triangle winding so FrontSide culling
103
+ // shows them. Swap corner 1 <-> 2 within each triangle BEFORE the quad-corner
104
+ // lookup; this reverses winding while keeping all 4 quad corners covered.
105
+ // (The previous override of viPos only collapsed both triangles to the same
106
+ // 3 corners, dropping quad corner vi=3 entirely.)
107
+ int effCorner = corner;
108
+ if (faceId == 4u || faceId == 5u) {
109
+ if (corner == 1) effCorner = 2;
110
+ else if (corner == 2) effCorner = 1;
111
+ }
112
+ int effViTotal = triangle * 3 + effCorner;
113
+ int vi = (diagonalFlag == 0u) ? VI_NORMAL[effViTotal] : VI_FLIPPED[effViTotal];
114
+
115
+ float u = float(vi & 1);
116
+ float v = float((vi >> 1) & 1);
117
+
118
+ // --- word0: position + face + tint + AO ---
119
+ uint lx = (a_w0) & 0xFu;
120
+ uint ly = (a_w0 >> 4u) & 0xFu;
121
+ uint lz = (a_w0 >> 8u) & 0xFu;
122
+ uint tint = (a_w0 >> 15u) & 0xFFu;
123
+
124
+ // AO: 2 bits per corner starting at bit 23
125
+ uint aoLevel = (a_w0 >> uint(23 + vi * 2)) & 0x3u;
126
+ v_ao = (float(aoLevel) + 1.0) / 4.0;
127
+
128
+ // --- word1: combined smooth light (8 bits per corner) ---
129
+ uint lightRaw = (a_w1 >> uint(vi * 8)) & 0xFFu;
130
+ v_light = float(lightRaw) / 255.0;
131
+
132
+ // --- word2: texture index ---
133
+ v_texIndex = int(a_w2 & 0xFFFu);
134
+ v_tintIndex = int(tint);
135
+ v_faceId = int(faceId);
136
+
137
+ // --- Per-face UV transform (legacy elemFaces + down +180°) ---
138
+ if (faceId == 0u) {
139
+ v_uv = vec2(u, 1.0 - v);
140
+ } else if (faceId == 1u) {
141
+ v_uv = vec2(1.0 - u, 1.0 - v);
142
+ } else if (faceId == 2u || faceId == 3u) {
143
+ v_uv = vec2(v, u);
144
+ } else if (faceId == 4u) {
145
+ v_uv = vec2(u, 1.0 - v);
146
+ } else { // faceId == 5u
147
+ v_uv = vec2(1.0 - u, 1.0 - v);
148
+ }
149
+
150
+ // --- Position: section base (multiples of 16) + face quad + block-local 0..15 ---
151
+ int sX = int(a_w3 & 0xFFFFu) - 32768;
152
+ int sZ = int((a_w3 >> 16u) & 0xFFFFu) - 32768;
153
+ int sY = int((a_w2 >> 13u) & 0x1Fu) - 4;
154
+ vec3 sectionBase = vec3(float(sX * 16), float(sY * 16), float(sZ * 16));
155
+ vec3 facePos = BASE[faceId] + u * DU[faceId] + v * DV[faceId];
156
+ vec3 blockLocal = vec3(float(lx), float(ly), float(lz));
157
+ // (sectionBase - u_cameraOrigin) is exact in float32; add small terms after.
158
+ vec3 relativePos = (sectionBase - u_cameraOrigin) + facePos + blockLocal - u_cameraOriginFrac;
159
+ vec4 mvPosition = modelViewMatrix * vec4(relativePos, 1.0);
160
+ gl_Position = projectionMatrix * mvPosition;
161
+
162
+ #ifdef USE_LOGDEPTHBUF
163
+ // Mirrors three.js logdepthbuf_vertex chunk (EXT path: fragment writes gl_FragDepth).
164
+ vFragDepth = 1.0 + gl_Position.w;
165
+ #endif
166
+
167
+ #ifdef USE_FOG
168
+ // Mirrors three.js fog_vertex chunk: view-space depth (positive in front of camera).
169
+ vFogDepth = -mvPosition.z;
170
+ #endif
171
+ }
172
+ `
173
+
174
+ const fragmentShader = /* glsl */ `
175
+ precision highp float;
176
+ precision highp int;
177
+
178
+ uniform sampler2D u_atlas;
179
+ uniform sampler2D u_tintPalette;
180
+ /** 0=normal 1=holes 2=tileIndex 3=faceId 4=atlasAlpha */
181
+ uniform float u_debugMode;
182
+
183
+ in float v_light;
184
+ in float v_ao;
185
+ in vec2 v_uv;
186
+ flat in int v_texIndex;
187
+ flat in int v_tintIndex;
188
+ flat in int v_faceId;
189
+
190
+ #ifdef USE_LOGDEPTHBUF
191
+ uniform float logDepthBufFC;
192
+ in float vFragDepth;
193
+ #endif
194
+
195
+ #ifdef USE_FOG
196
+ uniform vec3 fogColor;
197
+ in float vFogDepth;
198
+ #ifdef FOG_EXP2
199
+ uniform float fogDensity;
200
+ #else
201
+ uniform float fogNear;
202
+ uniform float fogFar;
203
+ #endif
204
+ #endif
205
+
206
+ out vec4 FragColor;
207
+
208
+ void writeLogDepth() {
209
+ #ifdef USE_LOGDEPTHBUF
210
+ // Camera is always perspective; skip the vIsPerspective branch from three.js
211
+ // standard chunks to avoid float-precision z-fight against neighbouring meshes.
212
+ gl_FragDepth = log2(vFragDepth) * logDepthBufFC * 0.5;
213
+ #endif
214
+ }
215
+
216
+ // Mirrors three.js fog_fragment chunk: applied after lighting/tint on the final colour.
217
+ void applyFog() {
218
+ #ifdef USE_FOG
219
+ #ifdef FOG_EXP2
220
+ float fogFactor = 1.0 - exp(-fogDensity * fogDensity * vFogDepth * vFogDepth);
221
+ #else
222
+ float fogFactor = smoothstep(fogNear, fogFar, vFogDepth);
223
+ #endif
224
+ FragColor.rgb = mix(FragColor.rgb, fogColor, fogFactor);
225
+ #endif
226
+ }
227
+
228
+ void main() {
229
+ // Atlas sample (pixelated, no filtering)
230
+ ivec2 atlasSize = textureSize(u_atlas, 0);
231
+ int tilesPerRow = atlasSize.x / 16;
232
+ ivec2 tileOrigin = ivec2(v_texIndex % tilesPerRow, v_texIndex / tilesPerRow) * 16;
233
+ ivec2 texel = tileOrigin + clamp(ivec2(v_uv * 16.0), ivec2(0), ivec2(15));
234
+ vec4 baseColor = texelFetch(u_atlas, texel, 0);
235
+
236
+ if (u_debugMode > 3.5) {
237
+ FragColor = vec4(vec3(baseColor.a), 1.0);
238
+ writeLogDepth();
239
+ return;
240
+ }
241
+
242
+ if (u_debugMode > 2.5) {
243
+ if (v_faceId == 0) FragColor = vec4(1.0, 0.0, 0.0, 1.0);
244
+ else if (v_faceId == 1) FragColor = vec4(0.0, 1.0, 0.0, 1.0);
245
+ else if (v_faceId == 2) FragColor = vec4(0.0, 0.0, 1.0, 1.0);
246
+ else if (v_faceId == 3) FragColor = vec4(1.0, 1.0, 0.0, 1.0);
247
+ else if (v_faceId == 4) FragColor = vec4(1.0, 0.0, 1.0, 1.0);
248
+ else FragColor = vec4(0.0, 1.0, 1.0, 1.0);
249
+ writeLogDepth();
250
+ return;
251
+ }
252
+
253
+ if (u_debugMode > 1.5) {
254
+ float t = float(v_texIndex) / 4095.0;
255
+ FragColor = vec4(t, fract(float(v_texIndex) / 64.0), 0.0, 1.0);
256
+ writeLogDepth();
257
+ return;
258
+ }
259
+
260
+ if (baseColor.a < 0.01) {
261
+ if (u_debugMode > 0.5) {
262
+ FragColor = vec4(1.0, 0.0, 0.0, 1.0);
263
+ writeLogDepth();
264
+ return;
265
+ }
266
+ discard;
267
+ }
268
+
269
+ // Tint from palette (256x1 RGBA texture, index 0 = white [1,1,1])
270
+ vec3 tint = texelFetch(u_tintPalette, ivec2(v_tintIndex, 0), 0).rgb;
271
+
272
+ // Combined light * AO, identity brightness curve (no mcBrightness) to match the
273
+ // legacy CPU mesher output 1:1.
274
+ float brightness = v_light * v_ao;
275
+
276
+ // Opaque full cubes: always alpha 1 (legacy uses cutout material; avoids seeing blocks behind)
277
+ FragColor = vec4(baseColor.rgb * tint * brightness, 1.0);
278
+ applyFog();
279
+ writeLogDepth();
280
+ }
281
+ `
282
+
283
+ export function createCubeBlockMaterial(): THREE.ShaderMaterial {
284
+ return new THREE.ShaderMaterial({
285
+ vertexShader,
286
+ fragmentShader,
287
+ // Merge UniformsLib.fog (fogColor/fogNear/fogFar/fogDensity) so Three.js's
288
+ // WebGLMaterials.refreshFogUniforms can keep them in sync with scene.fog each
289
+ // frame — only happens for materials with \`fog: true\` set below.
290
+ uniforms: THREE.UniformsUtils.merge([
291
+ THREE.UniformsLib.fog,
292
+ {
293
+ u_atlas: { value: null },
294
+ u_tintPalette: { value: null },
295
+ u_debugMode: { value: 0 },
296
+ u_cameraOrigin: { value: new THREE.Vector3() },
297
+ u_cameraOriginFrac: { value: new THREE.Vector3() },
298
+ },
299
+ ]),
300
+ // Opaque full cubes — WASM mesher already culls interior faces between
301
+ // solid neighbors, so no z-fighting between own faces. Keep NoBlending so
302
+ // the alpha=1 path writes pure pixels and depth.
303
+ transparent: false,
304
+ depthWrite: true,
305
+ depthTest: true,
306
+ blending: THREE.NoBlending,
307
+ glslVersion: THREE.GLSL3,
308
+ // Required for Three.js to inject \`#define USE_FOG\` (and refresh fog uniforms).
309
+ fog: true,
310
+ })
311
+ }
312
+
313
+ // Three geometry constants: 6 vertices per face (2 triangles, un-indexed)
314
+ export const VERTICES_PER_FACE = 6
315
+
316
+ // Word layout constants (for encoding/decoding instances)
317
+ export const WORD0 = {
318
+ LX_BITS: 4,
319
+ LY_BITS: 4,
320
+ LZ_BITS: 4,
321
+ FACE_BITS: 3,
322
+ TINT_BITS: 8,
323
+ AO_BITS_PER_CORNER: 2,
324
+ NUM_CORNERS: 4,
325
+ // Bit offsets
326
+ LX_SHIFT: 0,
327
+ LY_SHIFT: 4,
328
+ LZ_SHIFT: 8,
329
+ FACE_SHIFT: 12,
330
+ TINT_SHIFT: 15,
331
+ AO_SHIFT: 23,
332
+ TRANSPARENT_SHIFT: 31,
333
+ } as const
334
+
335
+ export const WORD1 = {
336
+ LIGHT_BITS_PER_CORNER: 8,
337
+ NUM_CORNERS: 4,
338
+ } as const
339
+
340
+ export const WORD2 = {
341
+ TEX_INDEX_BITS: 12,
342
+ DIAGONAL_FLAG_SHIFT: 12,
343
+ SECTION_Y_SHIFT: 13,
344
+ SECTION_Y_BITS: 5,
345
+ EMPTY_SHIFT: 18,
346
+ SPARE_BITS: 13,
347
+ } as const
348
+
349
+ /** Section base X/Z packed into a_w3 (16-block units, biased). */
350
+ export const WORD3 = {
351
+ SECTION_X_BITS: 16,
352
+ SECTION_Z_BITS: 16,
353
+ SECTION_BIAS: 32768,
354
+ } as const
@@ -0,0 +1,122 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Maps texture atlas positions to 12-bit absolute tile indices used by the
4
+ * instanced shader-cube path.
5
+ *
6
+ * Requirement: atlas MUST be 1024×1024 with 16×16 tiles (max 4096 tiles fits in 12 bits).
7
+ * If atlas dimensions differ, `isValid()` returns false and callers must fall back to
8
+ * the legacy vertex path.
9
+ */
10
+
11
+ // WorldBlockProvider interface (subset used by this module)
12
+ export interface TextureAtlasInfo {
13
+ /** Atlas width in pixels (must be 1024) */
14
+ width: number
15
+ /** Atlas height in pixels (must be 1024) */
16
+ height: number
17
+ /** Tile size in pixels (must be 16) */
18
+ tileSize: number
19
+ /** Resolution scale (suSv = tileSize * resolution) */
20
+ suSv: number
21
+ /** Texture name → atlas position */
22
+ textures: Record<string, TextureEntry>
23
+ }
24
+
25
+ export interface TextureEntry {
26
+ /** Horizontal pixel offset within atlas */
27
+ u: number
28
+ /** Vertical pixel offset within atlas */
29
+ v: number
30
+ /** Horizontal pixel count (typically 16 or suSv) */
31
+ su: number
32
+ /** Vertical pixel count (typically 16 or suSv) */
33
+ sv: number
34
+ }
35
+
36
+ export class TextureIndexMapping {
37
+ private atlasWidth: number
38
+ private atlasHeight: number
39
+ private tileSize: number
40
+ private tilesPerRow: number
41
+ private maxTiles: number
42
+ private valid: boolean
43
+
44
+ constructor(atlasInfo: TextureAtlasInfo) {
45
+ this.atlasWidth = atlasInfo.width
46
+ this.atlasHeight = atlasInfo.height
47
+ this.tileSize = atlasInfo.tileSize
48
+ this.tilesPerRow = Math.floor(this.atlasWidth / this.tileSize)
49
+ this.maxTiles = this.tilesPerRow * Math.floor(this.atlasHeight / this.tileSize)
50
+
51
+ // Only valid when atlas matches the shader's hardcoded layout (1024×1024, 16×16 tiles).
52
+ this.valid = this.atlasWidth === 1024
53
+ && this.atlasHeight === 1024
54
+ && this.tileSize === 16
55
+ && this.maxTiles <= 4096 // 12-bit limit
56
+ }
57
+
58
+ /** True when 12-bit texIndex encoding is safe (atlas matches the shader's layout). */
59
+ isValid(): boolean {
60
+ return this.valid
61
+ }
62
+
63
+ /** Tiles per row in the atlas */
64
+ getTilesPerRow(): number {
65
+ return this.tilesPerRow
66
+ }
67
+
68
+ /**
69
+ * Compute absolute tile index from atlas pixel position.
70
+ * Returns -1 if the position is out of range or gate fails.
71
+ */
72
+ tileIndexFromPixelCoords(u: number, v: number): number {
73
+ if (!this.valid) return -1
74
+ const tileCol = Math.floor(u / this.tileSize)
75
+ const tileRow = Math.floor(v / this.tileSize)
76
+ if (tileCol < 0 || tileCol >= this.tilesPerRow || tileRow < 0) return -1
77
+ const index = tileRow * this.tilesPerRow + tileCol
78
+ if (index >= this.maxTiles) return -1
79
+ return index
80
+ }
81
+
82
+ /**
83
+ * Get tile index for a texture entry from the atlas.
84
+ * Returns -1 if the texture spans multiple tiles or gate fails.
85
+ */
86
+ tileIndexFromTextureEntry(entry: TextureEntry): number {
87
+ if (!this.valid) return -1
88
+ // Verify the texture occupies exactly one tile
89
+ if (entry.su !== this.tileSize || entry.sv !== this.tileSize) return -1
90
+ return this.tileIndexFromPixelCoords(entry.u, entry.v)
91
+ }
92
+
93
+ /**
94
+ * Look up tile index by texture name.
95
+ * Returns -1 if the texture is not found, spans multiple tiles, or gate fails.
96
+ */
97
+ tileIndexFromTextureName(
98
+ textureName: string,
99
+ atlasInfo: TextureAtlasInfo,
100
+ ): number {
101
+ if (!this.valid) return -1
102
+ // Try exact name, then strip namespace prefix
103
+ let entry = atlasInfo.textures[textureName]
104
+ if (!entry && textureName.includes(':')) {
105
+ entry = atlasInfo.textures[textureName.split(':')[1]]
106
+ }
107
+ if (!entry && textureName.includes('/')) {
108
+ entry = atlasInfo.textures[textureName.split('/')[1]]
109
+ }
110
+ if (!entry) {
111
+ // Try 'block/' prefix
112
+ entry = atlasInfo.textures[`block/${textureName}`]
113
+ }
114
+ if (!entry) {
115
+ // Try without 'block/' prefix
116
+ const stripped = textureName.replace(/^block\//, '')
117
+ entry = atlasInfo.textures[stripped]
118
+ }
119
+ if (!entry) return -1
120
+ return this.tileIndexFromTextureEntry(entry)
121
+ }
122
+ }
@@ -0,0 +1,198 @@
1
+ //@ts-nocheck
2
+ import * as THREE from 'three'
3
+
4
+ // ---- Palette entry ----
5
+ export interface TintPaletteEntry {
6
+ r: number // 0-1
7
+ g: number
8
+ b: number
9
+ }
10
+
11
+ // ---- Palette manager ----
12
+ export class TintPalette {
13
+ private entries: TintPaletteEntry[] = [{ r: 1, g: 1, b: 1 }] // index 0 = white (no tint)
14
+ private colorToIndex: Map<number, number> = new Map() // packed color -> index
15
+ private categoryBiomeToIndex: Map<string, number> = new Map()
16
+ private texture: THREE.DataTexture | null = null
17
+ private ready = false
18
+
19
+ /** Pack [r,g,b] (0-1 floats) into a 24-bit integer for fast lookup */
20
+ private packColor(r: number, g: number, b: number): number {
21
+ const ri = Math.round(r * 255)
22
+ const gi = Math.round(g * 255)
23
+ const bi = Math.round(b * 255)
24
+ return (ri << 16) | (gi << 8) | bi
25
+ }
26
+
27
+ /** Add a tint entry, returns its palette index (reuses duplicates) */
28
+ add(r: number, g: number, b: number, category: string, key: string): number {
29
+ const packed = this.packColor(r, g, b)
30
+
31
+ // Try categorical lookup first (faster and preserves semantic grouping)
32
+ const catKey = `${category}:${key}`
33
+ const existing = this.categoryBiomeToIndex.get(catKey)
34
+ if (existing !== undefined) return existing
35
+
36
+ // Deduplicate by exact color
37
+ const colorIdx = this.colorToIndex.get(packed)
38
+ if (colorIdx !== undefined) {
39
+ this.categoryBiomeToIndex.set(catKey, colorIdx)
40
+ return colorIdx
41
+ }
42
+
43
+ const idx = this.entries.length
44
+ this.entries.push({ r, g, b })
45
+ this.colorToIndex.set(packed, idx)
46
+ this.categoryBiomeToIndex.set(catKey, idx)
47
+ return idx
48
+ }
49
+
50
+ /** Look up tint index for a specific block face */
51
+ getTintIndex(
52
+ faceTintIndex: number | undefined,
53
+ blockName: string,
54
+ blockProps: Record<string, any>,
55
+ biome: string,
56
+ ): number {
57
+ if (faceTintIndex === undefined) return 0 // white
58
+
59
+ if (faceTintIndex === 0) {
60
+ if (blockName === 'redstone_wire') {
61
+ return this.categoryBiomeToIndex.get(`redstone:${blockProps.power}`)
62
+ ?? this.categoryBiomeToIndex.get('redstone:0')
63
+ ?? 0
64
+ }
65
+ if (blockName === 'birch_leaves' || blockName === 'spruce_leaves' || blockName === 'lily_pad') {
66
+ return this.categoryBiomeToIndex.get(`constant:${blockName}`)
67
+ ?? this.categoryBiomeToIndex.get('constant:default')
68
+ ?? 0
69
+ }
70
+ if (blockName.includes('leaves') || blockName === 'vine') {
71
+ return this.categoryBiomeToIndex.get(`foliage:${biome}`) ?? this.categoryBiomeToIndex.get('foliage:plains') ?? 0
72
+ }
73
+ // Default: grass tint
74
+ return this.categoryBiomeToIndex.get(`grass:${biome}`) ?? this.categoryBiomeToIndex.get('grass:plains') ?? 0
75
+ }
76
+
77
+ return 0 // unknown tint index -> white
78
+ }
79
+
80
+ /** Get the palette entry at a given index */
81
+ getEntry(index: number): TintPaletteEntry {
82
+ return this.entries[index] ?? this.entries[0]
83
+ }
84
+
85
+ /** Total number of palette entries */
86
+ get size(): number {
87
+ return this.entries.length
88
+ }
89
+
90
+ /** Build RGBA Float32Array from palette entries (for DataTexture) */
91
+ private buildTextureData(): Float32Array {
92
+ // RGBA × 256 entries
93
+ const data = new Float32Array(256 * 4)
94
+ for (let i = 0; i < this.entries.length && i < 256; i++) {
95
+ const e = this.entries[i]
96
+ data[i * 4] = e.r
97
+ data[i * 4 + 1] = e.g
98
+ data[i * 4 + 2] = e.b
99
+ data[i * 4 + 3] = 1.0
100
+ }
101
+ return data
102
+ }
103
+
104
+ /** Create or update the Three.js DataTexture */
105
+ createTexture(): THREE.DataTexture {
106
+ if (this.texture) {
107
+ this.texture.dispose()
108
+ this.texture = null
109
+ this.ready = false
110
+ }
111
+ const data = this.buildTextureData()
112
+ const texture = new THREE.DataTexture(
113
+ data as any,
114
+ 256, 1,
115
+ THREE.RGBAFormat,
116
+ THREE.FloatType,
117
+ ) as THREE.DataTexture
118
+ texture.minFilter = THREE.NearestFilter
119
+ texture.magFilter = THREE.NearestFilter
120
+ texture.wrapS = THREE.ClampToEdgeWrapping
121
+ texture.wrapT = THREE.ClampToEdgeWrapping
122
+ texture.needsUpdate = true
123
+ this.texture = texture
124
+ this.ready = true
125
+ return texture
126
+ }
127
+
128
+ getTexture(): THREE.DataTexture | null {
129
+ return this.texture
130
+ }
131
+
132
+ isReady(): boolean {
133
+ return this.ready
134
+ }
135
+
136
+ /** Precompute the full palette using tints data */
137
+ static fromTintsData(tintsData: Record<string, any>): TintPalette {
138
+ const palette = new TintPalette()
139
+ // Offsets to avoid collision with categorical keys — already handled by packColor dedup
140
+
141
+ function tintToGl(tint: number): [number, number, number] {
142
+ const r = ((tint >> 16) & 0xff) / 255
143
+ const g = ((tint >> 8) & 0xff) / 255
144
+ const b = (tint & 0xff) / 255
145
+ return [r, g, b]
146
+ }
147
+
148
+ // --- grass tint (per biome) ---
149
+ if (tintsData.grass) {
150
+ const grassDefault = tintToGl(tintsData.grass.default)
151
+ for (const { keys, color } of tintsData.grass.data ?? []) {
152
+ const c = tintToGl(color as number)
153
+ for (const biome of keys as string[]) {
154
+ palette.add(c[0], c[1], c[2], 'grass', biome)
155
+ }
156
+ }
157
+ palette.add(grassDefault[0], grassDefault[1], grassDefault[2], 'grass', 'plains')
158
+ }
159
+
160
+ // --- foliage tint (per biome) ---
161
+ if (tintsData.foliage) {
162
+ const foliageDefault = tintToGl(tintsData.foliage.default)
163
+ for (const { keys, color } of tintsData.foliage.data ?? []) {
164
+ const c = tintToGl(color as number)
165
+ for (const biome of keys as string[]) {
166
+ palette.add(c[0], c[1], c[2], 'foliage', biome)
167
+ }
168
+ }
169
+ palette.add(foliageDefault[0], foliageDefault[1], foliageDefault[2], 'foliage', 'plains')
170
+ }
171
+
172
+ // --- redstone tint (per power level 0-15) ---
173
+ if (tintsData.redstone) {
174
+ const rsDefault = tintToGl(tintsData.redstone.default)
175
+ for (const { keys, color } of tintsData.redstone.data ?? []) {
176
+ const c = tintToGl(color as number)
177
+ for (const key of keys as string[]) {
178
+ palette.add(c[0], c[1], c[2], 'redstone', key)
179
+ }
180
+ }
181
+ palette.add(rsDefault[0], rsDefault[1], rsDefault[2], 'redstone', '0')
182
+ }
183
+
184
+ // --- constant tints (birch leaves, spruce leaves, lily pad) ---
185
+ if (tintsData.constant) {
186
+ const constDefault = tintToGl(tintsData.constant.default)
187
+ for (const { keys, color } of tintsData.constant.data ?? []) {
188
+ const c = tintToGl(color as number)
189
+ for (const key of keys as string[]) {
190
+ palette.add(c[0], c[1], c[2], 'constant', key)
191
+ }
192
+ }
193
+ palette.add(constDefault[0], constDefault[1], constDefault[2], 'constant', 'default')
194
+ }
195
+
196
+ return palette
197
+ }
198
+ }