minecraft-renderer 0.1.94 → 0.1.96
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.map +2 -2
- package/dist/mesherWasm.js +369 -196
- package/dist/minecraft-renderer.js +72 -73
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +420 -421
- package/package.json +1 -1
- package/src/mesher-shared/exportedGeometryTypes.ts +3 -1
- package/src/mesher-shared/shaderCubeFormat.ts +3 -0
- package/src/mesher-shared/shared.ts +2 -1
- package/src/three/entities.ts +3 -4
- package/src/three/entity/interpolationPolicy.test.ts +23 -1
- package/src/three/entity/interpolationPolicy.ts +66 -11
- package/src/three/entity/localVehicleRendering.integration.test.ts +30 -10
- package/src/three/entity/minecartLocalPassengerRendering.test.ts +4 -2
- package/src/three/entity/networkHeadPitchRendering.test.ts +1 -1
- package/src/three/entity/remoteBoatPassengerRotation.integration.test.ts +3 -1
- package/src/three/entity/vehiclePassengerProduction.test.ts +20 -5
- package/src/three/entity/vehiclePassengerUpdate.ts +11 -2
- package/src/three/shaders/cubeBlockShader.ts +75 -14
- package/src/three/worldGeometryExport.ts +18 -3
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +83 -30
- package/src/wasm-mesher/tests/shaderCubeUvParity.test.ts +226 -0
- package/src/wasm-mesher/worker/mesherWasm.ts +2 -1
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import blocksAtlasesJson from 'mc-assets/dist/blocksAtlases.json'
|
|
9
|
+
import { SHADER_CUBES_FORMAT_VERSION, SHADER_CUBES_WORDS_PER_FACE } from '../../mesher-shared/shaderCubeFormat'
|
|
9
10
|
import { WORD0, WORD1, WORD2, WORD3 } from '../../three/shaders/cubeBlockShader'
|
|
10
11
|
import { TextureIndexMapping, type TextureEntry } from '../../three/shaders/textureIndexMapping'
|
|
11
12
|
import { TintPalette } from '../../three/shaders/tintPalette'
|
|
12
13
|
|
|
13
|
-
export
|
|
14
|
-
export const SHADER_CUBES_WORDS_PER_FACE = 4 as const
|
|
14
|
+
export { SHADER_CUBES_FORMAT_VERSION, SHADER_CUBES_WORDS_PER_FACE } from '../../mesher-shared/shaderCubeFormat'
|
|
15
15
|
|
|
16
16
|
export type ShaderCubesOutput = {
|
|
17
17
|
words: Uint32Array
|
|
@@ -83,32 +83,78 @@ let tintPalette: TintPalette | null = null
|
|
|
83
83
|
let textureIndexMapping: TextureIndexMapping | null = null
|
|
84
84
|
let tintsMissingWarned = false
|
|
85
85
|
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
type FaceTextureRef = TextureEntry & { tileIndex?: number }
|
|
87
|
+
|
|
88
|
+
export type ResolvedFaceUv = { tileIndex: number; flipU: boolean; flipV: boolean }
|
|
89
|
+
|
|
90
|
+
const UV_RESOLVE_EPS = 1e-6
|
|
91
|
+
|
|
92
|
+
function isNearInteger(value: number): boolean {
|
|
93
|
+
return Math.abs(value - Math.round(value)) <= UV_RESOLVE_EPS
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve a face to (atlas tile + mirror flags).
|
|
98
|
+
* null ⇒ face does not fit exactly one aligned tile; block must use legacy path.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveFaceUv(tex: FaceTextureRef, texMapping: TextureIndexMapping): ResolvedFaceUv | null {
|
|
101
|
+
const tileSize = 16
|
|
88
102
|
let u = tex.u ?? 0
|
|
89
103
|
let v = tex.v ?? 0
|
|
90
104
|
let su = tex.su ?? tileSize
|
|
91
105
|
let sv = tex.sv ?? tileSize
|
|
92
|
-
if (u > 0 && u <= 1) u = Math.round(u * atlasWidth)
|
|
93
|
-
if (v > 0 && v <= 1) v = Math.round(v * atlasWidth)
|
|
94
|
-
if (Math.abs(su) > 0 && Math.abs(su) <= 1) su = Math.round(Math.abs(su) * atlasWidth) || tileSize
|
|
95
|
-
else if (su < 0) su = tileSize
|
|
96
|
-
if (Math.abs(sv) > 0 && Math.abs(sv) <= 1) sv = Math.round(Math.abs(sv) * atlasWidth) || tileSize
|
|
97
|
-
else if (sv < 0) sv = tileSize
|
|
98
|
-
return { u, v, su, sv }
|
|
99
|
-
}
|
|
100
106
|
|
|
101
|
-
|
|
107
|
+
if (!Number.isFinite(u) || !Number.isFinite(v) || !Number.isFinite(su) || !Number.isFinite(sv)) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
const atlasWidth = texMapping.getTilesPerRow() * tileSize
|
|
112
|
+
if (Math.abs(su) <= 1) {
|
|
113
|
+
u *= atlasWidth
|
|
114
|
+
v *= atlasWidth
|
|
115
|
+
su *= atlasWidth
|
|
116
|
+
sv *= atlasWidth
|
|
109
117
|
}
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
|
|
119
|
+
if (!isNearInteger(u) || !isNearInteger(v) || !isNearInteger(su) || !isNearInteger(sv)) {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const uPx = Math.round(u)
|
|
124
|
+
const vPx = Math.round(v)
|
|
125
|
+
const suPx = Math.round(su)
|
|
126
|
+
const svPx = Math.round(sv)
|
|
127
|
+
|
|
128
|
+
const flipU = suPx < 0
|
|
129
|
+
const flipV = svPx < 0
|
|
130
|
+
|
|
131
|
+
if (Math.abs(suPx) !== tileSize || Math.abs(svPx) !== tileSize) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const uMin = flipU ? uPx + suPx : uPx
|
|
136
|
+
const vMin = flipV ? vPx + svPx : vPx
|
|
137
|
+
|
|
138
|
+
if (uMin % tileSize !== 0 || vMin % tileSize !== 0) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
if (uMin < 0 || vMin < 0) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const derived = texMapping.tileIndexFromPixelCoords(uMin, vMin)
|
|
146
|
+
if (derived < 0) {
|
|
147
|
+
return null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (typeof tex.tileIndex === 'number') {
|
|
151
|
+
if (tex.tileIndex !== derived) {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
return { tileIndex: derived, flipU, flipV }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { tileIndex: derived, flipU, flipV }
|
|
112
158
|
}
|
|
113
159
|
|
|
114
160
|
/** Main thread + worker: use `loadedData` set by the app / mesher (see mesherWasm). */
|
|
@@ -158,8 +204,8 @@ export function resetShaderCubeResources(): void {
|
|
|
158
204
|
|
|
159
205
|
/**
|
|
160
206
|
* Returns true when the block is a plain 1×1×1 cube that the instanced shader path
|
|
161
|
-
* can render
|
|
162
|
-
*
|
|
207
|
+
* can render with pixel-parity UV (no model rotation, single un-rotated element with
|
|
208
|
+
* all 6 cardinal faces present, each face exactly one aligned atlas tile).
|
|
163
209
|
* Pass the already-resolved model variant (`modelVars[variantIndex][0]`).
|
|
164
210
|
*/
|
|
165
211
|
export function isShaderCubeBlock(
|
|
@@ -191,7 +237,7 @@ export function isShaderCubeBlock(
|
|
|
191
237
|
if ((eFace as { rotation?: number }).rotation) return false
|
|
192
238
|
const tex = eFace.texture
|
|
193
239
|
if (!tex) return false
|
|
194
|
-
if (
|
|
240
|
+
if (resolveFaceUv(tex as FaceTextureRef, texMapping) === null) {
|
|
195
241
|
return false
|
|
196
242
|
}
|
|
197
243
|
}
|
|
@@ -199,7 +245,7 @@ export function isShaderCubeBlock(
|
|
|
199
245
|
return true
|
|
200
246
|
}
|
|
201
247
|
|
|
202
|
-
function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[]): number {
|
|
248
|
+
function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex: number, ao: number[], flipV: boolean): number {
|
|
203
249
|
let w = 0
|
|
204
250
|
w |= (lx & 0xf) << WORD0.LX_SHIFT
|
|
205
251
|
w |= (ly & 0xf) << WORD0.LY_SHIFT
|
|
@@ -209,6 +255,9 @@ function packWord0(lx: number, ly: number, lz: number, faceId: number, tintIndex
|
|
|
209
255
|
for (let i = 0; i < WORD0.NUM_CORNERS; i++) {
|
|
210
256
|
w |= ((ao[i] ?? 3) & 3) << (WORD0.AO_SHIFT + i * WORD0.AO_BITS_PER_CORNER)
|
|
211
257
|
}
|
|
258
|
+
if (flipV) {
|
|
259
|
+
w |= 1 << WORD0.FLIP_V_SHIFT
|
|
260
|
+
}
|
|
212
261
|
return w >>> 0
|
|
213
262
|
}
|
|
214
263
|
|
|
@@ -224,7 +273,7 @@ function biasedSectionIndex(sectionBaseCoord: number): number {
|
|
|
224
273
|
return (Math.floor(sectionBaseCoord / 16) + WORD3.SECTION_BIAS) & WORD3.SECTION_MASK
|
|
225
274
|
}
|
|
226
275
|
|
|
227
|
-
export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number): number {
|
|
276
|
+
export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBaseX: number, sectionBaseY: number, sectionBaseZ: number, flipU = false): number {
|
|
228
277
|
let w = texIndex & ((1 << WORD2.TEX_INDEX_BITS) - 1)
|
|
229
278
|
if (aoDiagonalFlip) {
|
|
230
279
|
w |= 1 << WORD2.DIAGONAL_FLAG_SHIFT
|
|
@@ -235,6 +284,9 @@ export function packWord2(texIndex: number, aoDiagonalFlip: boolean, sectionBase
|
|
|
235
284
|
const sz = biasedSectionIndex(sectionBaseZ)
|
|
236
285
|
w |= ((sx >>> 16) & 0x3f) << WORD2.SECTION_X_HI_SHIFT
|
|
237
286
|
w |= ((sz >>> 16) & 0x3f) << WORD2.SECTION_Z_HI_SHIFT
|
|
287
|
+
if (flipU) {
|
|
288
|
+
w |= 1 << WORD2.FLIP_U_SHIFT
|
|
289
|
+
}
|
|
238
290
|
return w >>> 0
|
|
239
291
|
}
|
|
240
292
|
|
|
@@ -346,11 +398,12 @@ export function tryBuildShaderCubeInstances(
|
|
|
346
398
|
|
|
347
399
|
const eFace = faces[faceName]
|
|
348
400
|
const tex = eFace.texture! as FaceTextureRef
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
401
|
+
const resolved = resolveFaceUv(tex, textureIndexMapping)
|
|
402
|
+
if (resolved === null) {
|
|
351
403
|
words.length = wordsStart
|
|
352
404
|
return false
|
|
353
405
|
}
|
|
406
|
+
const { tileIndex: texIndex, flipU, flipV } = resolved
|
|
354
407
|
|
|
355
408
|
const rawAo = block.ao_data[faceDataIndex] ?? [3, 3, 3, 3]
|
|
356
409
|
const rawLight = lightCombinedForFace(block, faceDataIndex)
|
|
@@ -372,9 +425,9 @@ export function tryBuildShaderCubeInstances(
|
|
|
372
425
|
const tintIndex = tintPalette.getTintIndex(eFace.tintindex, cached.blockName, cached.blockProps, biome ?? 'plains')
|
|
373
426
|
|
|
374
427
|
words.push(
|
|
375
|
-
packWord0(lx, ly, lz, faceIdx, tintIndex, ao),
|
|
428
|
+
packWord0(lx, ly, lz, faceIdx, tintIndex, ao, flipV),
|
|
376
429
|
packWord1(lightCombined),
|
|
377
|
-
packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z),
|
|
430
|
+
packWord2(texIndex, aoDiagonalFlip, sectionOrigin.x, sectionOrigin.y, sectionOrigin.z, flipU),
|
|
378
431
|
packWord3(sectionOrigin.x, sectionOrigin.z)
|
|
379
432
|
)
|
|
380
433
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect, beforeEach, vi } from 'vitest'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { elemFaces } from '../../mesher-shared/modelsGeometryCommon'
|
|
5
|
+
import { SHADER_CUBES_FORMAT_VERSION } from '../../mesher-shared/shaderCubeFormat'
|
|
6
|
+
import { FACE_UV_TERMS, buildFaceUvGlsl, shaderCubeFaceUv, cubeBlockVertexShader, WORD0, WORD2 } from '../../three/shaders/cubeBlockShader'
|
|
7
|
+
import {
|
|
8
|
+
AO_LIGHT_REMAP,
|
|
9
|
+
resetShaderCubeResources,
|
|
10
|
+
getShaderCubeResources,
|
|
11
|
+
resolveFaceUv,
|
|
12
|
+
isShaderCubeBlock,
|
|
13
|
+
tryBuildShaderCubeInstances,
|
|
14
|
+
SHADER_CUBES_WORDS_PER_FACE
|
|
15
|
+
} from '../bridge/shaderCubeBridge'
|
|
16
|
+
import { createMeshesFromExport } from '../../three/worldGeometryExport'
|
|
17
|
+
|
|
18
|
+
const FACE_NAMES = ['up', 'down', 'east', 'west', 'south', 'north'] as const
|
|
19
|
+
const SIGN_COMBOS: Array<[boolean, boolean]> = [
|
|
20
|
+
[false, false],
|
|
21
|
+
[true, false],
|
|
22
|
+
[false, true],
|
|
23
|
+
[true, true]
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function requireShaderCubeResources() {
|
|
27
|
+
const resources = getShaderCubeResources()
|
|
28
|
+
if (!resources) throw new Error('shader cube resources unavailable in test')
|
|
29
|
+
return resources
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Legacy tile-local UV (render-from-wasm.ts elemFaces + down +180°). */
|
|
33
|
+
function legacyTileLocalUv(faceName: string, cornerIdx: number, suSign: number, svSign: number): [number, number] {
|
|
34
|
+
const [, , , cu, cv] = (elemFaces as Record<string, { corners: number[][] }>)[faceName]!.corners[cornerIdx]!
|
|
35
|
+
const r = faceName === 'down' ? 180 : 0
|
|
36
|
+
const uvcs = Math.cos((r * Math.PI) / 180)
|
|
37
|
+
const uvsn = -Math.sin((r * Math.PI) / 180)
|
|
38
|
+
const baseu = (cu - 0.5) * uvcs - (cv - 0.5) * uvsn + 0.5
|
|
39
|
+
const basev = (cu - 0.5) * uvsn + (cv - 0.5) * uvcs + 0.5
|
|
40
|
+
return [suSign < 0 ? 1 - baseu : baseu, svSign < 0 ? 1 - basev : basev]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
resetShaderCubeResources()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('shaderCubeFaceUv: 6 faces × 4 corners × 4 sign combos match legacy (96 cases)', () => {
|
|
48
|
+
for (let faceIdx = 0; faceIdx < 6; faceIdx++) {
|
|
49
|
+
const faceName = FACE_NAMES[faceIdx]!
|
|
50
|
+
for (let vi = 0; vi < 4; vi++) {
|
|
51
|
+
for (const [flipU, flipV] of SIGN_COMBOS) {
|
|
52
|
+
const suSign = flipU ? -1 : 1
|
|
53
|
+
const svSign = flipV ? -1 : 1
|
|
54
|
+
const elemCorner = AO_LIGHT_REMAP[faceIdx]![vi]!
|
|
55
|
+
const legacy = legacyTileLocalUv(faceName, elemCorner, suSign, svSign)
|
|
56
|
+
const shader = shaderCubeFaceUv(faceIdx, vi, flipU, flipV)
|
|
57
|
+
expect(shader[0]).toBeCloseTo(legacy[0], 9)
|
|
58
|
+
expect(shader[1]).toBeCloseTo(legacy[1], 9)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('buildFaceUvGlsl: 6 branches match FACE_UV_TERMS and vertex shader embeds generated table', () => {
|
|
65
|
+
const glsl = buildFaceUvGlsl()
|
|
66
|
+
for (let i = 0; i < 6; i++) {
|
|
67
|
+
expect(glsl).toContain(`faceId == ${i}u`)
|
|
68
|
+
const [ut, vt] = FACE_UV_TERMS[i]!
|
|
69
|
+
const glslU = ut === 'u' ? 'u' : ut === 'v' ? 'v' : ut === '1-u' ? '1.0 - u' : '1.0 - v'
|
|
70
|
+
const glslV = vt === 'u' ? 'u' : vt === 'v' ? 'v' : vt === '1-u' ? '1.0 - u' : '1.0 - v'
|
|
71
|
+
expect(glsl).toContain(`vec2(${glslU}, ${glslV})`)
|
|
72
|
+
}
|
|
73
|
+
expect(cubeBlockVertexShader).toContain(glsl)
|
|
74
|
+
expect(cubeBlockVertexShader).toContain('a_w2 >> 31u')
|
|
75
|
+
expect(cubeBlockVertexShader).toContain('a_w0 >> 31u')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('resolveFaceUv: crafting_table north/west regression', () => {
|
|
79
|
+
const { textureIndexMapping } = requireShaderCubeResources()
|
|
80
|
+
|
|
81
|
+
const north = resolveFaceUv({ u: 0.53125, v: 0, su: -0.015625, sv: 0.015625, tileIndex: 33 }, textureIndexMapping)
|
|
82
|
+
expect(north).toEqual({ tileIndex: 33, flipU: true, flipV: false })
|
|
83
|
+
|
|
84
|
+
const west = resolveFaceUv({ u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 }, textureIndexMapping)
|
|
85
|
+
expect(west).toEqual({ tileIndex: 33, flipU: false, flipV: false })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('tryBuildShaderCubeInstances: crafting_table north sets FLIP_U_SHIFT', () => {
|
|
89
|
+
const { textureIndexMapping, tintPalette } = requireShaderCubeResources()
|
|
90
|
+
const craftingFaces = {
|
|
91
|
+
up: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
|
|
92
|
+
down: { texture: { u: 0.53125, v: 0, su: -0.015625, sv: 0.015625, tileIndex: 33 } },
|
|
93
|
+
east: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
|
|
94
|
+
west: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
|
|
95
|
+
south: { texture: { u: 0.515625, v: 0, su: 0.015625, sv: 0.015625, tileIndex: 33 } },
|
|
96
|
+
north: { texture: { u: 0.53125, v: 0, su: -0.015625, sv: 0.015625, tileIndex: 33 } }
|
|
97
|
+
}
|
|
98
|
+
const model = { elements: [{ faces: craftingFaces }] }
|
|
99
|
+
const block = {
|
|
100
|
+
position: [0, 0, 0] as [number, number, number],
|
|
101
|
+
visible_faces: 1 << 5,
|
|
102
|
+
ao_data: [[3, 3, 3, 3]],
|
|
103
|
+
light_combined: [[255, 255, 255, 255]]
|
|
104
|
+
}
|
|
105
|
+
const words: number[] = []
|
|
106
|
+
const ok = tryBuildShaderCubeInstances(
|
|
107
|
+
block,
|
|
108
|
+
{ blockName: 'crafting_table', blockProps: {}, isCube: true, model },
|
|
109
|
+
model,
|
|
110
|
+
{ sectionOrigin: { x: 0, y: 0, z: 0 }, sectionHeight: 16, tintPalette, textureIndexMapping },
|
|
111
|
+
words
|
|
112
|
+
)
|
|
113
|
+
expect(ok).toBe(true)
|
|
114
|
+
expect(words.length).toBe(SHADER_CUBES_WORDS_PER_FACE)
|
|
115
|
+
expect(words[2]! & (1 << WORD2.FLIP_U_SHIFT)).not.toBe(0)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('tryBuildShaderCubeInstances: sv<0 up face sets FLIP_V_SHIFT', () => {
|
|
119
|
+
const { textureIndexMapping, tintPalette } = requireShaderCubeResources()
|
|
120
|
+
const stdFace = { u: 0, v: 0, su: 16, sv: 16 }
|
|
121
|
+
const upSvNeg = { u: 0.515625, v: 0.015625, su: 0.015625, sv: -0.015625, tileIndex: 33 }
|
|
122
|
+
expect(resolveFaceUv(upSvNeg, textureIndexMapping)).toEqual({ tileIndex: 33, flipU: false, flipV: true })
|
|
123
|
+
|
|
124
|
+
const model = {
|
|
125
|
+
elements: [
|
|
126
|
+
{
|
|
127
|
+
faces: {
|
|
128
|
+
up: { texture: upSvNeg },
|
|
129
|
+
down: { texture: stdFace },
|
|
130
|
+
east: { texture: stdFace },
|
|
131
|
+
west: { texture: stdFace },
|
|
132
|
+
south: { texture: stdFace },
|
|
133
|
+
north: { texture: stdFace }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
const block = {
|
|
139
|
+
position: [0, 0, 0] as [number, number, number],
|
|
140
|
+
visible_faces: 1 << 0,
|
|
141
|
+
ao_data: [[3, 3, 3, 3]],
|
|
142
|
+
light_combined: [[255, 255, 255, 255]]
|
|
143
|
+
}
|
|
144
|
+
const words: number[] = []
|
|
145
|
+
const ok = tryBuildShaderCubeInstances(
|
|
146
|
+
block,
|
|
147
|
+
{ blockName: 'observer', blockProps: { facing: 'north' }, isCube: true, model },
|
|
148
|
+
model,
|
|
149
|
+
{ sectionOrigin: { x: 0, y: 0, z: 0 }, sectionHeight: 16, tintPalette, textureIndexMapping },
|
|
150
|
+
words
|
|
151
|
+
)
|
|
152
|
+
expect(ok).toBe(true)
|
|
153
|
+
expect(words.length).toBe(SHADER_CUBES_WORDS_PER_FACE)
|
|
154
|
+
expect(words[0]! & (1 << WORD0.FLIP_V_SHIFT)).not.toBe(0)
|
|
155
|
+
expect(words[2]! & (1 << WORD2.FLIP_U_SHIFT)).toBe(0)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('resolveFaceUv gate: rejects crop, non-integer scale, misaligned tile, tileIndex mismatch, NaN', () => {
|
|
159
|
+
const { textureIndexMapping } = requireShaderCubeResources()
|
|
160
|
+
|
|
161
|
+
expect(resolveFaceUv({ u: 0, v: 0, su: 8, sv: 16 }, textureIndexMapping)).toBeNull()
|
|
162
|
+
expect(resolveFaceUv({ u: 0, v: 0, su: 15.6, sv: 16 }, textureIndexMapping)).toBeNull()
|
|
163
|
+
expect(resolveFaceUv({ u: 529, v: 0, su: 16, sv: 16 }, textureIndexMapping)).toBeNull()
|
|
164
|
+
expect(resolveFaceUv({ u: 0, v: 0, su: 16, sv: 16, tileIndex: 999 }, textureIndexMapping)).toBeNull()
|
|
165
|
+
expect(resolveFaceUv({ u: 0, v: 0, su: Number.NaN, sv: 16 }, textureIndexMapping)).toBeNull()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('isShaderCubeBlock: rejects faces that fail resolveFaceUv gate', () => {
|
|
169
|
+
const { textureIndexMapping } = requireShaderCubeResources()
|
|
170
|
+
const baseFaces = {
|
|
171
|
+
up: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
|
|
172
|
+
down: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
|
|
173
|
+
east: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
|
|
174
|
+
west: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
|
|
175
|
+
south: { texture: { u: 0, v: 0, su: 16, sv: 16 } },
|
|
176
|
+
north: { texture: { u: 0, v: 0, su: 16, sv: 16 } }
|
|
177
|
+
}
|
|
178
|
+
const model = { elements: [{ faces: baseFaces }] }
|
|
179
|
+
expect(isShaderCubeBlock({ blockName: 'stone', blockProps: {}, isCube: true, model }, model, 16, textureIndexMapping)).toBe(true)
|
|
180
|
+
|
|
181
|
+
const cropped = {
|
|
182
|
+
elements: [{ faces: { ...baseFaces, up: { texture: { u: 0, v: 0, su: 8, sv: 16 } } } }]
|
|
183
|
+
}
|
|
184
|
+
expect(isShaderCubeBlock({ blockName: 'stone', blockProps: {}, isCube: true, model: cropped }, cropped, 16, textureIndexMapping)).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
test('createMeshesFromExport: skips shader-cube sections with old formatVersion', () => {
|
|
188
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
189
|
+
const material = new THREE.MeshBasicMaterial()
|
|
190
|
+
const v3Words = new Uint32Array([1, 2, 3, 4])
|
|
191
|
+
const v4Words = new Uint32Array([5, 6, 7, 8])
|
|
192
|
+
|
|
193
|
+
const groups = createMeshesFromExport(
|
|
194
|
+
{
|
|
195
|
+
version: 'test',
|
|
196
|
+
exportedAt: '',
|
|
197
|
+
camera: { position: { x: 0, y: 0, z: 0 }, rotation: { pitch: 0, yaw: 0 } },
|
|
198
|
+
sections: [
|
|
199
|
+
{
|
|
200
|
+
key: 'old',
|
|
201
|
+
position: { x: 0, y: 0, z: 0 },
|
|
202
|
+
geometry: { positions: [], normals: [], colors: [], skyLights: [], blockLights: [], uvs: [], indices: [] },
|
|
203
|
+
shaderCubes: { words: v3Words, count: 1, formatVersion: 3 as unknown as typeof SHADER_CUBES_FORMAT_VERSION }
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
key: 'new',
|
|
207
|
+
position: { x: 16, y: 0, z: 0 },
|
|
208
|
+
geometry: { positions: [], normals: [], colors: [], skyLights: [], blockLights: [], uvs: [], indices: [] },
|
|
209
|
+
shaderCubes: { words: v4Words, count: 1, formatVersion: SHADER_CUBES_FORMAT_VERSION }
|
|
210
|
+
}
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
material,
|
|
214
|
+
new THREE.ShaderMaterial({ vertexShader: 'void main() {}', fragmentShader: 'void main() {}' })
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
expect(groups).toHaveLength(1)
|
|
218
|
+
expect(groups[0]!.children).toHaveLength(1)
|
|
219
|
+
expect(warn).toHaveBeenCalledOnce()
|
|
220
|
+
expect(warn.mock.calls[0]![0]).toContain('Skipped 1 shader-cube section')
|
|
221
|
+
expect(warn.mock.calls[0]![0]).toContain('3')
|
|
222
|
+
expect(warn.mock.calls[0]![0]).toContain(String(SHADER_CUBES_FORMAT_VERSION))
|
|
223
|
+
|
|
224
|
+
warn.mockRestore()
|
|
225
|
+
material.dispose()
|
|
226
|
+
})
|
|
@@ -4,6 +4,7 @@ import { convertChunkToWasm, getBlockMeta, type ChunkConversionResult } from '..
|
|
|
4
4
|
import { extractColumnHeightmap, splitColumnWasmOutputToSections } from '../bridge/render-from-wasm'
|
|
5
5
|
import { setBlockStatesData as setMesherData } from '../../mesher-shared/models'
|
|
6
6
|
import { defaultMesherConfig, type MesherGeometryOutput, SECTION_HEIGHT } from '../../mesher-shared/shared'
|
|
7
|
+
import { SHADER_CUBES_FORMAT_VERSION } from '../../mesher-shared/shaderCubeFormat'
|
|
7
8
|
import { packVisibilitySet, VISIBILITY_SET_ALL_TRUE } from '../../mesher-shared/visibilitySet'
|
|
8
9
|
import { VisGraph } from '../../mesher-shared/visGraph'
|
|
9
10
|
import { worldColumnKey, World } from '../../mesher-shared/world'
|
|
@@ -1532,7 +1533,7 @@ function processColumnTick() {
|
|
|
1532
1533
|
geometry.shaderCubes = {
|
|
1533
1534
|
words: new Uint32Array(exported.shaderCubes.words),
|
|
1534
1535
|
count: exported.shaderCubes.count,
|
|
1535
|
-
formatVersion:
|
|
1536
|
+
formatVersion: SHADER_CUBES_FORMAT_VERSION
|
|
1536
1537
|
}
|
|
1537
1538
|
}
|
|
1538
1539
|
if (exported.blendGeometry && hasBlendMesh) {
|