minecraft-renderer 0.1.48 → 0.1.50

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 +2 -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/three/chunkMeshManager.ts +312 -39
  15. package/src/three/globalBlockBuffer.ts +292 -0
  16. package/src/three/menuBackground/config.ts +1 -1
  17. package/src/three/menuBackground/defaultOptions.ts +52 -19
  18. package/src/three/menuBackground/index.ts +5 -1
  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 +56 -23
  28. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  29. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +399 -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,399 @@
1
+ //@ts-nocheck
2
+ /**
3
+ * Pack visible WASM block faces into GPU-instanced shader words (4×Uint32 per face).
4
+ * Each emitted instance becomes one face quad on the shader-cube mesh; the legacy
5
+ * vertex path is bypassed for blocks that pass {@link isShaderCubeBlock}.
6
+ */
7
+
8
+ import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
9
+ import { WORD0, WORD1, WORD2, WORD3 } from '../../three/shaders/cubeBlockShader'
10
+ import { TextureIndexMapping, type TextureEntry } from '../../three/shaders/textureIndexMapping'
11
+ import { TintPalette } from '../../three/shaders/tintPalette'
12
+
13
+ export const SHADER_CUBES_FORMAT_VERSION = 2 as const
14
+ export const SHADER_CUBES_WORDS_PER_FACE = 4 as const
15
+
16
+ export type ShaderCubesOutput = {
17
+ words: Uint32Array
18
+ /** Number of visible faces (= instances × 1; each instance is one face) */
19
+ count: number
20
+ formatVersion: typeof SHADER_CUBES_FORMAT_VERSION
21
+ }
22
+
23
+ const CARDINAL_FACE_NAMES = ['up', 'down', 'east', 'west', 'south', 'north'] as const
24
+ const WASM_FACE_ORDER = CARDINAL_FACE_NAMES
25
+ const FACE_NAME_TO_INDEX: Record<string, number> = {
26
+ up: 0,
27
+ down: 1,
28
+ east: 2,
29
+ west: 3,
30
+ south: 4,
31
+ north: 5,
32
+ }
33
+
34
+ /**
35
+ * WASM/elemFaces corner order → shader vi corner order (BASE/DU/DV in cubeBlockShader).
36
+ * UP/DOWN/EAST/WEST match 1:1; NORTH/SOUTH need a 180° corner rotation because their
37
+ * BASE origin is on the opposite side of the quad.
38
+ */
39
+ export const AO_LIGHT_REMAP: readonly (readonly number[])[] = [
40
+ [0, 1, 2, 3], // UP
41
+ [0, 1, 2, 3], // DOWN
42
+ [0, 1, 2, 3], // EAST
43
+ [0, 1, 2, 3], // WEST
44
+ [2, 3, 0, 1], // SOUTH
45
+ [2, 3, 0, 1], // NORTH
46
+ ] as const
47
+
48
+ /** Reorder per-corner AO or light values for shader-space corners. */
49
+ export function remapCornersForShaderFace(
50
+ faceIdx: number,
51
+ values: number[],
52
+ fallback: number,
53
+ ): number[] {
54
+ const map = AO_LIGHT_REMAP[faceIdx] ?? AO_LIGHT_REMAP[0]
55
+ return map.map((i) => values[i] ?? fallback)
56
+ }
57
+
58
+ export interface ShaderCubeBlockInput {
59
+ position: [number, number, number]
60
+ visible_faces: number
61
+ ao_data: number[][]
62
+ light_data: number[][]
63
+ light_combined?: number[][]
64
+ }
65
+
66
+ export interface ShaderCubeModelInput {
67
+ blockName: string
68
+ blockProps: Record<string, any>
69
+ isCube: boolean
70
+ /** Resolved variant `models[variantIndex][0]` */
71
+ model: {
72
+ x?: number
73
+ y?: number
74
+ z?: number
75
+ elements?: Array<{
76
+ rotation?: { axis: string, angle: number, origin: number[] }
77
+ faces?: Record<string, { texture?: TextureEntry & { rotation?: number }, tintindex?: number }>
78
+ }>
79
+ }
80
+ }
81
+
82
+ let tintPalette: TintPalette | null = null
83
+ let textureIndexMapping: TextureIndexMapping | null = null
84
+
85
+ /** Convert mc-assets texture scales (normalized or negative) to pixel tile size for index lookup. */
86
+ function normalizeTextureEntryForTileIndex(
87
+ tex: { u?: number, v?: number, su?: number, sv?: number },
88
+ atlasWidth: number,
89
+ tileSize: number,
90
+ ): TextureEntry {
91
+ let u = tex.u ?? 0
92
+ let v = tex.v ?? 0
93
+ let su = tex.su ?? tileSize
94
+ let sv = tex.sv ?? tileSize
95
+ if (u > 0 && u <= 1) u = Math.round(u * atlasWidth)
96
+ if (v > 0 && v <= 1) v = Math.round(v * atlasWidth)
97
+ if (Math.abs(su) > 0 && Math.abs(su) <= 1) su = Math.round(Math.abs(su) * atlasWidth) || tileSize
98
+ else if (su < 0) su = tileSize
99
+ if (Math.abs(sv) > 0 && Math.abs(sv) <= 1) sv = Math.round(Math.abs(sv) * atlasWidth) || tileSize
100
+ else if (sv < 0) sv = tileSize
101
+ return { u, v, su, sv }
102
+ }
103
+
104
+ type FaceTextureRef = TextureEntry & { tileIndex?: number }
105
+
106
+ /** Prefer atlas `tileIndex` from block model (legacy path uses the same). */
107
+ export function resolveFaceTileIndex(
108
+ tex: FaceTextureRef,
109
+ texMapping: TextureIndexMapping,
110
+ ): number {
111
+ const fromAtlas = tex.tileIndex
112
+ // tile 0 is a special/atlas-padding slot; use pixel fallback for block faces
113
+ if (typeof fromAtlas === 'number' && fromAtlas > 0 && fromAtlas < 4096) {
114
+ return fromAtlas
115
+ }
116
+ const entry = normalizeTextureEntryForTileIndex(
117
+ tex,
118
+ texMapping.getTilesPerRow() * 16,
119
+ 16,
120
+ )
121
+ return texMapping.tileIndexFromTextureEntry(entry)
122
+ }
123
+
124
+ /** Main thread + worker: use `loadedData` set by the app / mesher (see mesherWasm). */
125
+ function getTintsJson(): Record<string, any> {
126
+ const tints = (globalThis as any).loadedData?.tints
127
+ if (!tints) {
128
+ throw new Error('shaderCubeBridge: globalThis.loadedData.tints is not available yet')
129
+ }
130
+ return tints
131
+ }
132
+
133
+ export function getShaderCubeResources(): {
134
+ tintPalette: TintPalette
135
+ textureIndexMapping: TextureIndexMapping
136
+ } {
137
+ if (!tintPalette) {
138
+ tintPalette = TintPalette.fromTintsData(getTintsJson())
139
+ tintPalette.createTexture()
140
+ }
141
+ if (!textureIndexMapping) {
142
+ const latest = (blocksAtlasesJson as any).latest ?? (blocksAtlasesJson as any)
143
+ textureIndexMapping = new TextureIndexMapping({
144
+ width: latest.width,
145
+ height: latest.height,
146
+ tileSize: latest.tileSize ?? 16,
147
+ suSv: latest.suSv ?? 16,
148
+ textures: latest.textures ?? {},
149
+ })
150
+ }
151
+ return { tintPalette, textureIndexMapping }
152
+ }
153
+
154
+ /** Reset cached palette/atlas (tests). */
155
+ export function resetShaderCubeResources(): void {
156
+ tintPalette = null
157
+ textureIndexMapping = null
158
+ }
159
+
160
+ /**
161
+ * Returns true when the block is a plain 1×1×1 cube that the instanced shader path
162
+ * can render exactly like the legacy mesher (no model rotation, single un-rotated
163
+ * element with all 6 cardinal faces present, atlas matches shader gate).
164
+ * Pass the already-resolved model variant (`modelVars[variantIndex][0]`).
165
+ */
166
+ export function isShaderCubeBlock(
167
+ cached: ShaderCubeModelInput & { isCube: boolean },
168
+ model: ShaderCubeModelInput['model'],
169
+ sectionHeight: number,
170
+ texMapping: TextureIndexMapping,
171
+ ): boolean {
172
+ if (sectionHeight !== 16) return false
173
+ if (!cached.isCube) return false
174
+ if (!texMapping.isValid()) return false
175
+
176
+ for (const axis of ['x', 'y', 'z'] as const) {
177
+ if (model[axis]) return false
178
+ }
179
+
180
+ const elements = model.elements ?? []
181
+ if (elements.length !== 1) return false
182
+ const element = elements[0]
183
+ if (element.rotation) return false
184
+
185
+ const faces = element.faces
186
+ if (!faces) return false
187
+
188
+ for (const faceName of CARDINAL_FACE_NAMES) {
189
+ const eFace = faces[faceName]
190
+ if (!eFace) return false
191
+ // Shader UV table is hardcoded per-face; arbitrary per-face UV rotation forces legacy.
192
+ if ((eFace as { rotation?: number }).rotation) return false
193
+ const tex = eFace.texture
194
+ if (!tex) return false
195
+ if (resolveFaceTileIndex(tex as FaceTextureRef, texMapping) < 0) {
196
+ return false
197
+ }
198
+ }
199
+
200
+ return true
201
+ }
202
+
203
+ function packWord0(
204
+ lx: number,
205
+ ly: number,
206
+ lz: number,
207
+ faceId: number,
208
+ tintIndex: number,
209
+ ao: number[],
210
+ ): number {
211
+ let w = 0
212
+ w |= (lx & 0xf) << WORD0.LX_SHIFT
213
+ w |= (ly & 0xf) << WORD0.LY_SHIFT
214
+ w |= (lz & 0xf) << WORD0.LZ_SHIFT
215
+ w |= (faceId & 7) << WORD0.FACE_SHIFT
216
+ w |= (tintIndex & 0xff) << WORD0.TINT_SHIFT
217
+ for (let i = 0; i < WORD0.NUM_CORNERS; i++) {
218
+ w |= ((ao[i] ?? 3) & 3) << (WORD0.AO_SHIFT + i * WORD0.AO_BITS_PER_CORNER)
219
+ }
220
+ return w >>> 0
221
+ }
222
+
223
+ function packWord1(lightCombined: number[]): number {
224
+ let w = 0
225
+ for (let i = 0; i < WORD1.NUM_CORNERS; i++) {
226
+ w |= ((lightCombined[i] ?? 255) & 0xff) << (i * WORD1.LIGHT_BITS_PER_CORNER)
227
+ }
228
+ return w >>> 0
229
+ }
230
+
231
+ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseY: number): number {
232
+ let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
233
+ if (aoDiagonalFlip) {
234
+ w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
235
+ }
236
+ const sectionY = ((Math.floor(sectionBaseY / 16) + 4) & 0x1f) << WORD2.SECTION_Y_SHIFT
237
+ w |= sectionY
238
+ return w >>> 0
239
+ }
240
+
241
+ export function packWord3(sectionBaseX: number, sectionBaseZ: number): number {
242
+ const sx = (Math.floor(sectionBaseX / 16) + WORD3.SECTION_BIAS) & 0xffff
243
+ const sz = (Math.floor(sectionBaseZ / 16) + WORD3.SECTION_BIAS) & 0xffff
244
+ return (sx | (sz << 16)) >>> 0
245
+ }
246
+
247
+ /** Decode section base block coords from packed words (round-trip helper for tests). */
248
+ export function decodeSectionBaseFromWords(word2: number, word3: number): { x: number, y: number, z: number } {
249
+ const sX = (word3 & 0xffff) - WORD3.SECTION_BIAS
250
+ const sZ = ((word3 >>> 16) & 0xffff) - WORD3.SECTION_BIAS
251
+ const sY = ((word2 >>> WORD2.SECTION_Y_SHIFT) & ((1 << WORD2.SECTION_Y_BITS) - 1)) - 4
252
+ return { x: sX * 16, y: sY * 16, z: sZ * 16 }
253
+ }
254
+
255
+ /** EMPTY sentinel for a freed global-buffer instance slot. */
256
+ export function packWord2Empty(): number {
257
+ return (1 << WORD2.EMPTY_SHIFT) >>> 0
258
+ }
259
+
260
+ /** 12-bit texture tile index from packed word2. */
261
+ export function unpackTexIndexFromWord2(word2: number): number {
262
+ return word2 & ((1 << WORD2.TEX_INDEX_BITS) - 1)
263
+ }
264
+
265
+ function lightCombinedForFace(
266
+ block: ShaderCubeBlockInput,
267
+ faceDataIndex: number,
268
+ ): number[] {
269
+ const packed = block.light_combined?.[faceDataIndex]
270
+ if (packed && packed.length === 4) {
271
+ return packed
272
+ }
273
+ const floats = block.light_data[faceDataIndex] ?? [1, 1, 1, 1]
274
+ return floats.map((f) => {
275
+ if (f >= 1) return 255
276
+ return Math.min(255, Math.round(f * 255))
277
+ })
278
+ }
279
+
280
+ function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number> {
281
+ const map: Record<number, number> = {}
282
+ let dataIndex = 0
283
+ for (const faceName of WASM_FACE_ORDER) {
284
+ const faceIdx = FACE_NAME_TO_INDEX[faceName]
285
+ if ((visibleFaces & (1 << faceIdx)) !== 0) {
286
+ map[faceIdx] = dataIndex++
287
+ }
288
+ }
289
+ return map
290
+ }
291
+
292
+ export type BuildShaderCubeInstancesOpts = {
293
+ sectionOrigin: { x: number, y: number, z: number }
294
+ sectionHeight: number
295
+ biome?: string
296
+ tintPalette: TintPalette
297
+ textureIndexMapping: TextureIndexMapping
298
+ /**
299
+ * When false (blocks with model.ao === false), emit full-bright faces without AO
300
+ * diagonal flip — matches legacy render-from-wasm path.
301
+ */
302
+ doAO?: boolean
303
+ }
304
+
305
+ /**
306
+ * Pack all visible faces of one block into `words` (4 uints per face).
307
+ * Returns false if the block must use the legacy vertex path.
308
+ */
309
+ export function tryBuildShaderCubeInstances(
310
+ block: ShaderCubeBlockInput,
311
+ cached: ShaderCubeModelInput & { isCube: boolean },
312
+ model: ShaderCubeModelInput['model'],
313
+ opts: BuildShaderCubeInstancesOpts,
314
+ words: number[],
315
+ ): boolean {
316
+ const { sectionOrigin, sectionHeight, biome, tintPalette, textureIndexMapping, doAO = true } = opts
317
+
318
+ if (!isShaderCubeBlock(cached, model, sectionHeight, textureIndexMapping)) {
319
+ return false
320
+ }
321
+
322
+ const element = model.elements![0]
323
+ const faces = element.faces!
324
+ const wasmFaceToDataIndex = buildWasmFaceToDataIndex(block.visible_faces)
325
+ const [bx, by, bz] = block.position
326
+ const lx = bx & 15
327
+ const ly = (by - sectionOrigin.y) & 15
328
+ const lz = bz & 15
329
+
330
+ const wordsStart = words.length
331
+
332
+ for (const faceName of WASM_FACE_ORDER) {
333
+ const faceIdx = FACE_NAME_TO_INDEX[faceName]
334
+ if ((block.visible_faces & (1 << faceIdx)) === 0) continue
335
+
336
+ const faceDataIndex = wasmFaceToDataIndex[faceIdx]
337
+ if (faceDataIndex === undefined) continue
338
+
339
+ const eFace = faces[faceName]
340
+ const tex = eFace.texture! as FaceTextureRef
341
+ const texIndex = resolveFaceTileIndex(tex, textureIndexMapping)
342
+ if (texIndex < 0) {
343
+ words.length = wordsStart
344
+ return false
345
+ }
346
+
347
+ const rawAo = block.ao_data[faceDataIndex] ?? [3, 3, 3, 3]
348
+ const rawLight = lightCombinedForFace(block, faceDataIndex)
349
+
350
+ let ao: number[]
351
+ let lightCombined: number[]
352
+ let aoDiagonalFlip: boolean
353
+
354
+ if (doAO) {
355
+ ao = remapCornersForShaderFace(faceIdx, rawAo, 3)
356
+ lightCombined = remapCornersForShaderFace(faceIdx, rawLight, 255)
357
+ aoDiagonalFlip = ao[0] + ao[3] >= ao[1] + ao[2]
358
+ } else {
359
+ ao = [3, 3, 3, 3]
360
+ lightCombined = [255, 255, 255, 255]
361
+ aoDiagonalFlip = false
362
+ }
363
+
364
+ const tintIndex = tintPalette.getTintIndex(
365
+ eFace.tintindex,
366
+ cached.blockName,
367
+ cached.blockProps,
368
+ biome ?? 'plains',
369
+ )
370
+
371
+ words.push(
372
+ packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
373
+ packWord1(lightCombined),
374
+ packWord2(texIndex, aoDiagonalFlip, sectionOrigin.y),
375
+ packWord3(sectionOrigin.x, sectionOrigin.z),
376
+ )
377
+ }
378
+
379
+ return true
380
+ }
381
+
382
+ export function buildShaderCubesFromWords(wordQuads: number[]): ShaderCubesOutput | undefined {
383
+ const faceCount = Math.floor(wordQuads.length / SHADER_CUBES_WORDS_PER_FACE)
384
+ if (faceCount === 0) return undefined
385
+ return {
386
+ words: new Uint32Array(wordQuads),
387
+ count: faceCount,
388
+ formatVersion: SHADER_CUBES_FORMAT_VERSION,
389
+ }
390
+ }
391
+
392
+ /** Visible face count from WASM bitmask */
393
+ export function countVisibleFaces(visibleFaces: number): number {
394
+ let n = 0
395
+ for (let i = 0; i < 6; i++) {
396
+ if (visibleFaces & (1 << i)) n++
397
+ }
398
+ return n
399
+ }
@@ -0,0 +1,58 @@
1
+ //@ts-nocheck
2
+ import { test, expect } from 'vitest'
3
+ import {
4
+ computeShaderSectionRaycastAabb,
5
+ raycastAabb,
6
+ raycastSectionAabb,
7
+ } from '../../three/sectionRaycastAabb'
8
+ import { SHADER_CUBES_WORDS_PER_FACE } from '../bridge/shaderCubeBridge'
9
+ import { WORD0 } from '../../three/shaders/cubeBlockShader'
10
+
11
+ test('raycastSectionAabb: hit along +X (full 16³)', () => {
12
+ const t = raycastSectionAabb(0, 0, 0, 1, 0, 0, 16, 0, 0, 100)
13
+ expect(t).toBeDefined()
14
+ expect(t!).toBeGreaterThanOrEqual(8)
15
+ expect(t!).toBeLessThanOrEqual(24)
16
+ })
17
+
18
+ test('raycastSectionAabb: miss behind ray', () => {
19
+ expect(raycastSectionAabb(0, 0, 0, 1, 0, 0, -32, 0, 0, 100)).toBeUndefined()
20
+ })
21
+
22
+ test('raycastAabb: respects maxDist', () => {
23
+ expect(raycastAabb(0, 0, 0, 1, 0, 0, 100, 0, 0, 108, 0, 8, 10)).toBeUndefined()
24
+ })
25
+
26
+ test('computeShaderSectionRaycastAabb: tight box from one block at local (5,3,7)', () => {
27
+ const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE)
28
+ const lx = 5
29
+ const ly = 3
30
+ const lz = 7
31
+ words[0] = lx | (ly << WORD0.LY_SHIFT) | (lz << WORD0.LZ_SHIFT)
32
+ const box = computeShaderSectionRaycastAabb(words, 1, 8, 8, 8)!
33
+ expect(box.minX).toBe(lx)
34
+ expect(box.maxX).toBe(lx + 1)
35
+ expect(box.minY).toBe(ly)
36
+ expect(box.maxY).toBe(ly + 1)
37
+ expect(box.minZ).toBe(lz)
38
+ expect(box.maxZ).toBe(lz + 1)
39
+
40
+ const t = raycastAabb(-1, 3.5, 7.5, 1, 0, 0, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, 100)
41
+ expect(t).toBe(6)
42
+ })
43
+
44
+ test('raycastAabb: origin inside box is ignored', () => {
45
+ const t = raycastAabb(5.5, 3.5, 7.5, 0, 0, 1, 5, 3, 7, 6, 4, 8, 100)
46
+ expect(t).toBeUndefined()
47
+ })
48
+
49
+ test('raycastAabb: narrow floor slab blocks downward ray', () => {
50
+ const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE * 2)
51
+ words[0] = 4 | (0 << WORD0.LY_SHIFT) | (4 << WORD0.LZ_SHIFT)
52
+ words[4] = 5 | (0 << WORD0.LY_SHIFT) | (4 << WORD0.LZ_SHIFT)
53
+ const box = computeShaderSectionRaycastAabb(words, 2, 8, 8, 8)!
54
+ const t = raycastAabb(4.5, 10, 4.5, 0, -1, 0, box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, 20)
55
+ expect(t).toBeDefined()
56
+ expect(t!).toBeGreaterThan(0)
57
+ expect(t!).toBeLessThan(10)
58
+ })