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.
- 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 +2 -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/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 +52 -19
- package/src/three/menuBackground/index.ts +5 -1
- 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 +56 -23
- package/src/wasm-mesher/bridge/render-from-wasm.ts +62 -185
- package/src/wasm-mesher/bridge/shaderCubeBridge.ts +399 -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,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
|
+
}
|
|
Binary file
|
|
@@ -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
|
+
})
|