minecraft-renderer 0.1.95 → 0.1.97
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 +75 -76
- 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 +133 -22
- package/src/three/graphicsBackendBase.ts +1 -0
- 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
|
@@ -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) {
|