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.
- package/dist/mesher.js +1 -1
- package/dist/mesher.js.map +2 -2
- package/dist/mesherWasm.js +3740 -183
- package/dist/minecraft-renderer.js +332 -60
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +705 -433
- package/package.json +1 -1
- package/src/graphicsBackend/config.ts +4 -0
- package/src/graphicsBackend/playerState.ts +1 -0
- package/src/graphicsBackend/rendererOptionsSync.ts +1 -0
- package/src/lib/worldrendererCommon.ts +13 -0
- package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
- package/src/mesher-shared/shared.ts +8 -0
- package/src/playerState/playerState.ts +2 -0
- package/src/three/chunkMeshManager.ts +312 -39
- package/src/three/globalBlockBuffer.ts +292 -0
- package/src/three/menuBackground/config.ts +1 -1
- package/src/three/menuBackground/defaultOptions.ts +27 -19
- package/src/three/modules/sciFiWorldReveal.ts +162 -68
- package/src/three/modules/starfield.ts +9 -1
- package/src/three/sectionRaycastAabb.ts +167 -0
- package/src/three/shaderCubeMesh.ts +93 -0
- package/src/three/shaders/cubeBlockShader.ts +354 -0
- package/src/three/shaders/textureIndexMapping.ts +122 -0
- package/src/three/shaders/tintPalette.ts +198 -0
- package/src/three/worldGeometryExport.ts +53 -25
- package/src/three/worldRendererThree.ts +93 -26
- package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +396 -0
- package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
- package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
- package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
- package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
- 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
|
+
}
|