minecraft-renderer 0.1.48 → 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 (32) 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/lib/worldrendererCommon.ts +13 -0
  11. package/src/mesher-shared/exportedGeometryTypes.ts +5 -1
  12. package/src/mesher-shared/shared.ts +8 -0
  13. package/src/three/chunkMeshManager.ts +312 -39
  14. package/src/three/globalBlockBuffer.ts +292 -0
  15. package/src/three/menuBackground/config.ts +1 -1
  16. package/src/three/menuBackground/defaultOptions.ts +18 -18
  17. package/src/three/modules/sciFiWorldReveal.ts +162 -68
  18. package/src/three/modules/starfield.ts +9 -1
  19. package/src/three/sectionRaycastAabb.ts +167 -0
  20. package/src/three/shaderCubeMesh.ts +93 -0
  21. package/src/three/shaders/cubeBlockShader.ts +354 -0
  22. package/src/three/shaders/textureIndexMapping.ts +122 -0
  23. package/src/three/shaders/tintPalette.ts +198 -0
  24. package/src/three/worldGeometryExport.ts +53 -25
  25. package/src/three/worldRendererThree.ts +56 -23
  26. package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
  27. package/src/wasm-mesher/bridge/shaderCubeBridge.ts +396 -0
  28. package/src/wasm-mesher/runtime-build/wasm_mesher_bg.wasm +0 -0
  29. package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +58 -0
  30. package/src/wasm-mesher/tests/shaderCubeInstances.test.ts +360 -0
  31. package/src/wasm-mesher/tests/splitColumnWasmOutput.test.ts +11 -4
  32. package/src/wasm-mesher/worker/mesherWasm.ts +17 -2
@@ -0,0 +1,396 @@
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
+ /** Tints via esbuild-data (worker plugin, web-client rsbuild alias, vitest alias). */
125
+ function getTintsJson(): Record<string, any> {
126
+ const mod = require('esbuild-data') as { tints?: Record<string, any>, default?: { tints?: Record<string, any> } }
127
+ return mod.tints ?? mod.default?.tints ?? {}
128
+ }
129
+
130
+ export function getShaderCubeResources(): {
131
+ tintPalette: TintPalette
132
+ textureIndexMapping: TextureIndexMapping
133
+ } {
134
+ if (!tintPalette) {
135
+ tintPalette = TintPalette.fromTintsData(getTintsJson())
136
+ tintPalette.createTexture()
137
+ }
138
+ if (!textureIndexMapping) {
139
+ const latest = (blocksAtlasesJson as any).latest ?? (blocksAtlasesJson as any)
140
+ textureIndexMapping = new TextureIndexMapping({
141
+ width: latest.width,
142
+ height: latest.height,
143
+ tileSize: latest.tileSize ?? 16,
144
+ suSv: latest.suSv ?? 16,
145
+ textures: latest.textures ?? {},
146
+ })
147
+ }
148
+ return { tintPalette, textureIndexMapping }
149
+ }
150
+
151
+ /** Reset cached palette/atlas (tests). */
152
+ export function resetShaderCubeResources(): void {
153
+ tintPalette = null
154
+ textureIndexMapping = null
155
+ }
156
+
157
+ /**
158
+ * Returns true when the block is a plain 1×1×1 cube that the instanced shader path
159
+ * can render exactly like the legacy mesher (no model rotation, single un-rotated
160
+ * element with all 6 cardinal faces present, atlas matches shader gate).
161
+ * Pass the already-resolved model variant (`modelVars[variantIndex][0]`).
162
+ */
163
+ export function isShaderCubeBlock(
164
+ cached: ShaderCubeModelInput & { isCube: boolean },
165
+ model: ShaderCubeModelInput['model'],
166
+ sectionHeight: number,
167
+ texMapping: TextureIndexMapping,
168
+ ): boolean {
169
+ if (sectionHeight !== 16) return false
170
+ if (!cached.isCube) return false
171
+ if (!texMapping.isValid()) return false
172
+
173
+ for (const axis of ['x', 'y', 'z'] as const) {
174
+ if (model[axis]) return false
175
+ }
176
+
177
+ const elements = model.elements ?? []
178
+ if (elements.length !== 1) return false
179
+ const element = elements[0]
180
+ if (element.rotation) return false
181
+
182
+ const faces = element.faces
183
+ if (!faces) return false
184
+
185
+ for (const faceName of CARDINAL_FACE_NAMES) {
186
+ const eFace = faces[faceName]
187
+ if (!eFace) return false
188
+ // Shader UV table is hardcoded per-face; arbitrary per-face UV rotation forces legacy.
189
+ if ((eFace as { rotation?: number }).rotation) return false
190
+ const tex = eFace.texture
191
+ if (!tex) return false
192
+ if (resolveFaceTileIndex(tex as FaceTextureRef, texMapping) < 0) {
193
+ return false
194
+ }
195
+ }
196
+
197
+ return true
198
+ }
199
+
200
+ function packWord0(
201
+ lx: number,
202
+ ly: number,
203
+ lz: number,
204
+ faceId: number,
205
+ tintIndex: number,
206
+ ao: number[],
207
+ ): number {
208
+ let w = 0
209
+ w |= (lx & 0xf) << WORD0.LX_SHIFT
210
+ w |= (ly & 0xf) << WORD0.LY_SHIFT
211
+ w |= (lz & 0xf) << WORD0.LZ_SHIFT
212
+ w |= (faceId & 7) << WORD0.FACE_SHIFT
213
+ w |= (tintIndex & 0xff) << WORD0.TINT_SHIFT
214
+ for (let i = 0; i < WORD0.NUM_CORNERS; i++) {
215
+ w |= ((ao[i] ?? 3) & 3) << (WORD0.AO_SHIFT + i * WORD0.AO_BITS_PER_CORNER)
216
+ }
217
+ return w >>> 0
218
+ }
219
+
220
+ function packWord1(lightCombined: number[]): number {
221
+ let w = 0
222
+ for (let i = 0; i < WORD1.NUM_CORNERS; i++) {
223
+ w |= ((lightCombined[i] ?? 255) & 0xff) << (i * WORD1.LIGHT_BITS_PER_CORNER)
224
+ }
225
+ return w >>> 0
226
+ }
227
+
228
+ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseY: number): number {
229
+ let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
230
+ if (aoDiagonalFlip) {
231
+ w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
232
+ }
233
+ const sectionY = ((Math.floor(sectionBaseY / 16) + 4) & 0x1f) << WORD2.SECTION_Y_SHIFT
234
+ w |= sectionY
235
+ return w >>> 0
236
+ }
237
+
238
+ export function packWord3(sectionBaseX: number, sectionBaseZ: number): number {
239
+ const sx = (Math.floor(sectionBaseX / 16) + WORD3.SECTION_BIAS) & 0xffff
240
+ const sz = (Math.floor(sectionBaseZ / 16) + WORD3.SECTION_BIAS) & 0xffff
241
+ return (sx | (sz << 16)) >>> 0
242
+ }
243
+
244
+ /** Decode section base block coords from packed words (round-trip helper for tests). */
245
+ export function decodeSectionBaseFromWords(word2: number, word3: number): { x: number, y: number, z: number } {
246
+ const sX = (word3 & 0xffff) - WORD3.SECTION_BIAS
247
+ const sZ = ((word3 >>> 16) & 0xffff) - WORD3.SECTION_BIAS
248
+ const sY = ((word2 >>> WORD2.SECTION_Y_SHIFT) & ((1 << WORD2.SECTION_Y_BITS) - 1)) - 4
249
+ return { x: sX * 16, y: sY * 16, z: sZ * 16 }
250
+ }
251
+
252
+ /** EMPTY sentinel for a freed global-buffer instance slot. */
253
+ export function packWord2Empty(): number {
254
+ return (1 << WORD2.EMPTY_SHIFT) >>> 0
255
+ }
256
+
257
+ /** 12-bit texture tile index from packed word2. */
258
+ export function unpackTexIndexFromWord2(word2: number): number {
259
+ return word2 & ((1 << WORD2.TEX_INDEX_BITS) - 1)
260
+ }
261
+
262
+ function lightCombinedForFace(
263
+ block: ShaderCubeBlockInput,
264
+ faceDataIndex: number,
265
+ ): number[] {
266
+ const packed = block.light_combined?.[faceDataIndex]
267
+ if (packed && packed.length === 4) {
268
+ return packed
269
+ }
270
+ const floats = block.light_data[faceDataIndex] ?? [1, 1, 1, 1]
271
+ return floats.map((f) => {
272
+ if (f >= 1) return 255
273
+ return Math.min(255, Math.round(f * 255))
274
+ })
275
+ }
276
+
277
+ function buildWasmFaceToDataIndex(visibleFaces: number): Record<number, number> {
278
+ const map: Record<number, number> = {}
279
+ let dataIndex = 0
280
+ for (const faceName of WASM_FACE_ORDER) {
281
+ const faceIdx = FACE_NAME_TO_INDEX[faceName]
282
+ if ((visibleFaces & (1 << faceIdx)) !== 0) {
283
+ map[faceIdx] = dataIndex++
284
+ }
285
+ }
286
+ return map
287
+ }
288
+
289
+ export type BuildShaderCubeInstancesOpts = {
290
+ sectionOrigin: { x: number, y: number, z: number }
291
+ sectionHeight: number
292
+ biome?: string
293
+ tintPalette: TintPalette
294
+ textureIndexMapping: TextureIndexMapping
295
+ /**
296
+ * When false (blocks with model.ao === false), emit full-bright faces without AO
297
+ * diagonal flip — matches legacy render-from-wasm path.
298
+ */
299
+ doAO?: boolean
300
+ }
301
+
302
+ /**
303
+ * Pack all visible faces of one block into `words` (4 uints per face).
304
+ * Returns false if the block must use the legacy vertex path.
305
+ */
306
+ export function tryBuildShaderCubeInstances(
307
+ block: ShaderCubeBlockInput,
308
+ cached: ShaderCubeModelInput & { isCube: boolean },
309
+ model: ShaderCubeModelInput['model'],
310
+ opts: BuildShaderCubeInstancesOpts,
311
+ words: number[],
312
+ ): boolean {
313
+ const { sectionOrigin, sectionHeight, biome, tintPalette, textureIndexMapping, doAO = true } = opts
314
+
315
+ if (!isShaderCubeBlock(cached, model, sectionHeight, textureIndexMapping)) {
316
+ return false
317
+ }
318
+
319
+ const element = model.elements![0]
320
+ const faces = element.faces!
321
+ const wasmFaceToDataIndex = buildWasmFaceToDataIndex(block.visible_faces)
322
+ const [bx, by, bz] = block.position
323
+ const lx = bx & 15
324
+ const ly = (by - sectionOrigin.y) & 15
325
+ const lz = bz & 15
326
+
327
+ const wordsStart = words.length
328
+
329
+ for (const faceName of WASM_FACE_ORDER) {
330
+ const faceIdx = FACE_NAME_TO_INDEX[faceName]
331
+ if ((block.visible_faces & (1 << faceIdx)) === 0) continue
332
+
333
+ const faceDataIndex = wasmFaceToDataIndex[faceIdx]
334
+ if (faceDataIndex === undefined) continue
335
+
336
+ const eFace = faces[faceName]
337
+ const tex = eFace.texture! as FaceTextureRef
338
+ const texIndex = resolveFaceTileIndex(tex, textureIndexMapping)
339
+ if (texIndex < 0) {
340
+ words.length = wordsStart
341
+ return false
342
+ }
343
+
344
+ const rawAo = block.ao_data[faceDataIndex] ?? [3, 3, 3, 3]
345
+ const rawLight = lightCombinedForFace(block, faceDataIndex)
346
+
347
+ let ao: number[]
348
+ let lightCombined: number[]
349
+ let aoDiagonalFlip: boolean
350
+
351
+ if (doAO) {
352
+ ao = remapCornersForShaderFace(faceIdx, rawAo, 3)
353
+ lightCombined = remapCornersForShaderFace(faceIdx, rawLight, 255)
354
+ aoDiagonalFlip = ao[0] + ao[3] >= ao[1] + ao[2]
355
+ } else {
356
+ ao = [3, 3, 3, 3]
357
+ lightCombined = [255, 255, 255, 255]
358
+ aoDiagonalFlip = false
359
+ }
360
+
361
+ const tintIndex = tintPalette.getTintIndex(
362
+ eFace.tintindex,
363
+ cached.blockName,
364
+ cached.blockProps,
365
+ biome ?? 'plains',
366
+ )
367
+
368
+ words.push(
369
+ packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
370
+ packWord1(lightCombined),
371
+ packWord2(texIndex, aoDiagonalFlip, sectionOrigin.y),
372
+ packWord3(sectionOrigin.x, sectionOrigin.z),
373
+ )
374
+ }
375
+
376
+ return true
377
+ }
378
+
379
+ export function buildShaderCubesFromWords(wordQuads: number[]): ShaderCubesOutput | undefined {
380
+ const faceCount = Math.floor(wordQuads.length / SHADER_CUBES_WORDS_PER_FACE)
381
+ if (faceCount === 0) return undefined
382
+ return {
383
+ words: new Uint32Array(wordQuads),
384
+ count: faceCount,
385
+ formatVersion: SHADER_CUBES_FORMAT_VERSION,
386
+ }
387
+ }
388
+
389
+ /** Visible face count from WASM bitmask */
390
+ export function countVisibleFaces(visibleFaces: number): number {
391
+ let n = 0
392
+ for (let i = 0; i < 6; i++) {
393
+ if (visibleFaces & (1 << i)) n++
394
+ }
395
+ return n
396
+ }
@@ -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
+ })