minecraft-renderer 0.1.80 → 0.1.82
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/README.md +37 -1
- package/dist/mesher.js +19 -19
- package/dist/mesher.js.map +3 -3
- package/dist/mesherWasm.js +167 -167
- package/dist/minecraft-renderer.js +71 -71
- package/dist/minecraft-renderer.js.meta.json +1 -1
- package/dist/threeWorker.js +432 -432
- package/package.json +1 -1
- package/src/lib/worldrendererCommon.ts +11 -0
- package/src/mesher-shared/models.ts +2 -30
- package/src/mesher-shared/modelsGeometryCommon.ts +22 -0
- package/src/mesher-shared/standaloneRenderer.ts +2 -5
- package/src/three/cameraCollisionBlockCache.ts +156 -0
- package/src/three/cameraCollisionSolidity.ts +50 -0
- package/src/three/chunkMeshManager.ts +4 -128
- package/src/three/globalLegacyBuffer.ts +0 -107
- package/src/three/sectionRaycastAabb.ts +0 -98
- package/src/three/tests/chunkMeshManagerLegacy.test.ts +0 -30
- package/src/three/thirdPersonVoxelRaycast.ts +120 -0
- package/src/three/worldRendererThree.ts +30 -40
- package/src/wasm-mesher/bridge/render-from-wasm.ts +3 -5
- package/src/wasm-mesher/tests/cameraCollisionBlockCache.test.ts +82 -0
- package/src/wasm-mesher/tests/sectionRaycastAabb.test.ts +1 -106
- package/src/wasm-mesher/tests/thirdPersonVoxelRaycast.test.ts +63 -0
|
@@ -95,31 +95,6 @@ export function isPointInsideAabb(
|
|
|
95
95
|
return ox >= minX && ox <= maxX && oy >= minY && oy <= maxY && oz >= minZ && oz <= maxZ
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/** True if a `far`-bounded ray from (ox,oy,oz) dir (dx,dy,dz) crosses or starts inside
|
|
99
|
-
* the cube-section AABB centered at (cx,cy,cz) with the given half-extent. */
|
|
100
|
-
export function sectionAabbIntersectsRay(
|
|
101
|
-
cx: number,
|
|
102
|
-
cy: number,
|
|
103
|
-
cz: number,
|
|
104
|
-
ox: number,
|
|
105
|
-
oy: number,
|
|
106
|
-
oz: number,
|
|
107
|
-
dx: number,
|
|
108
|
-
dy: number,
|
|
109
|
-
dz: number,
|
|
110
|
-
far: number,
|
|
111
|
-
halfExtent: number
|
|
112
|
-
): boolean {
|
|
113
|
-
const minX = cx - halfExtent
|
|
114
|
-
const minY = cy - halfExtent
|
|
115
|
-
const minZ = cz - halfExtent
|
|
116
|
-
const maxX = cx + halfExtent
|
|
117
|
-
const maxY = cy + halfExtent
|
|
118
|
-
const maxZ = cz + halfExtent
|
|
119
|
-
if (isPointInsideAabb(ox, oy, oz, minX, minY, minZ, maxX, maxY, maxZ)) return true
|
|
120
|
-
return raycastAabb(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, far) !== undefined
|
|
121
|
-
}
|
|
122
|
-
|
|
123
98
|
/** Ray–AABB entry distance, or undefined. Ignores hits when origin is inside the box. */
|
|
124
99
|
export function raycastAabb(
|
|
125
100
|
ox: number,
|
|
@@ -229,76 +204,3 @@ export function raycastAabbFromInside(
|
|
|
229
204
|
|
|
230
205
|
return tExit <= maxDist ? tExit : undefined
|
|
231
206
|
}
|
|
232
|
-
|
|
233
|
-
/** Per-block raycast; `word0Stride` 1 = GlobalBlockBuffer SoA, 4 = deferred AoS. */
|
|
234
|
-
export function raycastShaderBlocksAabb(
|
|
235
|
-
w0Source: Uint32Array,
|
|
236
|
-
start: number,
|
|
237
|
-
faceCount: number,
|
|
238
|
-
word0Stride: number,
|
|
239
|
-
sectionCenterX: number,
|
|
240
|
-
sectionCenterY: number,
|
|
241
|
-
sectionCenterZ: number,
|
|
242
|
-
ox: number,
|
|
243
|
-
oy: number,
|
|
244
|
-
oz: number,
|
|
245
|
-
dx: number,
|
|
246
|
-
dy: number,
|
|
247
|
-
dz: number,
|
|
248
|
-
maxDist: number,
|
|
249
|
-
visitGen: Uint16Array,
|
|
250
|
-
visitStamp: number
|
|
251
|
-
): number | undefined {
|
|
252
|
-
const baseX = sectionCenterX - 8
|
|
253
|
-
const baseY = sectionCenterY - 8
|
|
254
|
-
const baseZ = sectionCenterZ - 8
|
|
255
|
-
|
|
256
|
-
let closest = maxDist
|
|
257
|
-
let found = false
|
|
258
|
-
|
|
259
|
-
for (let i = 0; i < faceCount; i++) {
|
|
260
|
-
const w0 = w0Source[start + i * word0Stride]!
|
|
261
|
-
const lx = w0 & ((1 << WORD0.LX_BITS) - 1)
|
|
262
|
-
const ly = (w0 >> WORD0.LY_SHIFT) & ((1 << WORD0.LY_BITS) - 1)
|
|
263
|
-
const lz = (w0 >> WORD0.LZ_SHIFT) & ((1 << WORD0.LZ_BITS) - 1)
|
|
264
|
-
const visitIdx = lx + (ly << 4) + (lz << 8)
|
|
265
|
-
if (visitGen[visitIdx] === visitStamp) continue
|
|
266
|
-
visitGen[visitIdx] = visitStamp
|
|
267
|
-
|
|
268
|
-
const minX = baseX + lx
|
|
269
|
-
const minY = baseY + ly
|
|
270
|
-
const minZ = baseZ + lz
|
|
271
|
-
const maxX = minX + 1
|
|
272
|
-
const maxY = minY + 1
|
|
273
|
-
const maxZ = minZ + 1
|
|
274
|
-
|
|
275
|
-
let t: number | undefined
|
|
276
|
-
if (isPointInsideAabb(ox, oy, oz, minX, minY, minZ, maxX, maxY, maxZ)) {
|
|
277
|
-
t = raycastAabbFromInside(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, closest)
|
|
278
|
-
} else {
|
|
279
|
-
t = raycastAabb(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, closest)
|
|
280
|
-
}
|
|
281
|
-
if (t !== undefined && t < closest) {
|
|
282
|
-
closest = t
|
|
283
|
-
found = true
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return found ? closest : undefined
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/** 16³ section box centered at (cx, cy, cz) — tests / legacy helper. */
|
|
291
|
-
export function raycastSectionAabb(
|
|
292
|
-
ox: number,
|
|
293
|
-
oy: number,
|
|
294
|
-
oz: number,
|
|
295
|
-
dx: number,
|
|
296
|
-
dy: number,
|
|
297
|
-
dz: number,
|
|
298
|
-
cx: number,
|
|
299
|
-
cy: number,
|
|
300
|
-
cz: number,
|
|
301
|
-
maxDist: number
|
|
302
|
-
): number | undefined {
|
|
303
|
-
return raycastAabb(ox, oy, oz, dx, dy, dz, cx - 8, cy - 8, cz - 8, cx + 8, cy + 8, cz + 8, maxDist)
|
|
304
|
-
}
|
|
@@ -296,36 +296,6 @@ test('ChunkMeshManager: invalid blend geometry falls back to pooled mesh', () =>
|
|
|
296
296
|
manager.dispose()
|
|
297
297
|
})
|
|
298
298
|
|
|
299
|
-
test('ChunkMeshManager: raycastGlobalLegacySections rejects off-ray sections within center distance', () => {
|
|
300
|
-
const manager = createManager()
|
|
301
|
-
const onRayKey = '0,0,0'
|
|
302
|
-
const offRayKey = '0,2,0'
|
|
303
|
-
|
|
304
|
-
manager.updateSection(onRayKey, makeBlendOnlyGeometry())
|
|
305
|
-
|
|
306
|
-
const offRayGeo = makeBlendOnlyGeometry()
|
|
307
|
-
offRayGeo.sz = 40
|
|
308
|
-
manager.updateSection(offRayKey, offRayGeo)
|
|
309
|
-
|
|
310
|
-
expect(manager.globalLegacyBlendBuffer?.hasSection(onRayKey)).toBe(true)
|
|
311
|
-
expect(manager.globalLegacyBlendBuffer?.hasSection(offRayKey)).toBe(true)
|
|
312
|
-
expect(manager.sectionObjects[offRayKey]?.worldZ).toBe(40)
|
|
313
|
-
|
|
314
|
-
const origin = new THREE.Vector3(4, 8, 8)
|
|
315
|
-
const direction = new THREE.Vector3(1, 0, 0).normalize()
|
|
316
|
-
const raycaster = new THREE.Raycaster(origin, direction)
|
|
317
|
-
raycaster.far = 4
|
|
318
|
-
|
|
319
|
-
const hit = manager.raycastGlobalLegacySections(raycaster, origin, 80)
|
|
320
|
-
expect(hit).toBeDefined()
|
|
321
|
-
expect(hit!).toBeGreaterThan(2)
|
|
322
|
-
expect(hit!).toBeLessThan(4)
|
|
323
|
-
|
|
324
|
-
manager.cleanupSection(onRayKey)
|
|
325
|
-
manager.cleanupSection(offRayKey)
|
|
326
|
-
manager.dispose()
|
|
327
|
-
})
|
|
328
|
-
|
|
329
299
|
test('ChunkMeshManager: mixed opaque and blend route to separate global buffers', () => {
|
|
330
300
|
const manager = createManager()
|
|
331
301
|
const key = '0,0,0'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { raycastAabb, raycastAabbFromInside } from './sectionRaycastAabb'
|
|
3
|
+
|
|
4
|
+
export type VoxelRaycastHit = {
|
|
5
|
+
distance: number
|
|
6
|
+
blockX: number
|
|
7
|
+
blockY: number
|
|
8
|
+
blockZ: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const intBound = (s: number, ds: number): number => {
|
|
12
|
+
if (ds === 0) return Infinity
|
|
13
|
+
if (ds < 0) return intBound(-s, -ds)
|
|
14
|
+
const frac = s - Math.floor(s)
|
|
15
|
+
return (1 - frac) / ds
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const hitSolidBlock = (
|
|
19
|
+
ox: number,
|
|
20
|
+
oy: number,
|
|
21
|
+
oz: number,
|
|
22
|
+
dx: number,
|
|
23
|
+
dy: number,
|
|
24
|
+
dz: number,
|
|
25
|
+
bx: number,
|
|
26
|
+
by: number,
|
|
27
|
+
bz: number,
|
|
28
|
+
maxDist: number,
|
|
29
|
+
radius: number,
|
|
30
|
+
minCameraDistance: number
|
|
31
|
+
): number | undefined => {
|
|
32
|
+
const minX = bx - radius
|
|
33
|
+
const minY = by - radius
|
|
34
|
+
const minZ = bz - radius
|
|
35
|
+
const maxX = bx + 1 + radius
|
|
36
|
+
const maxY = by + 1 + radius
|
|
37
|
+
const maxZ = bz + 1 + radius
|
|
38
|
+
|
|
39
|
+
let t = raycastAabb(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, maxDist)
|
|
40
|
+
if (t === undefined) {
|
|
41
|
+
t = raycastAabbFromInside(ox, oy, oz, dx, dy, dz, minX, minY, minZ, maxX, maxY, maxZ, maxDist)
|
|
42
|
+
}
|
|
43
|
+
if (t === undefined) return undefined
|
|
44
|
+
return Math.max(minCameraDistance, t)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Amanatides–Woo grid traversal against solid block volumes (not rendered mesh faces).
|
|
49
|
+
* `radius` is baked into per-block AABB expansion (swept sphere).
|
|
50
|
+
*/
|
|
51
|
+
export function raycastVoxelSolid(
|
|
52
|
+
ox: number,
|
|
53
|
+
oy: number,
|
|
54
|
+
oz: number,
|
|
55
|
+
dx: number,
|
|
56
|
+
dy: number,
|
|
57
|
+
dz: number,
|
|
58
|
+
maxDist: number,
|
|
59
|
+
radius: number,
|
|
60
|
+
minCameraDistance: number,
|
|
61
|
+
isSolid: (x: number, y: number, z: number) => boolean
|
|
62
|
+
): VoxelRaycastHit | undefined {
|
|
63
|
+
if (maxDist <= 0) return undefined
|
|
64
|
+
|
|
65
|
+
let x = Math.floor(ox)
|
|
66
|
+
let y = Math.floor(oy)
|
|
67
|
+
let z = Math.floor(oz)
|
|
68
|
+
|
|
69
|
+
const stepX = dx > 0 ? 1 : dx < 0 ? -1 : 0
|
|
70
|
+
const stepY = dy > 0 ? 1 : dy < 0 ? -1 : 0
|
|
71
|
+
const stepZ = dz > 0 ? 1 : dz < 0 ? -1 : 0
|
|
72
|
+
|
|
73
|
+
const tDeltaX = stepX !== 0 ? Math.abs(1 / dx) : Infinity
|
|
74
|
+
const tDeltaY = stepY !== 0 ? Math.abs(1 / dy) : Infinity
|
|
75
|
+
const tDeltaZ = stepZ !== 0 ? Math.abs(1 / dz) : Infinity
|
|
76
|
+
|
|
77
|
+
let tMaxX = stepX !== 0 ? intBound(ox, dx) : Infinity
|
|
78
|
+
let tMaxY = stepY !== 0 ? intBound(oy, dy) : Infinity
|
|
79
|
+
let tMaxZ = stepZ !== 0 ? intBound(oz, dz) : Infinity
|
|
80
|
+
|
|
81
|
+
const tryHit = (bx: number, by: number, bz: number): VoxelRaycastHit | undefined => {
|
|
82
|
+
if (!isSolid(bx, by, bz)) return undefined
|
|
83
|
+
const distance = hitSolidBlock(ox, oy, oz, dx, dy, dz, bx, by, bz, maxDist, radius, minCameraDistance)
|
|
84
|
+
if (distance === undefined || distance > maxDist) return undefined
|
|
85
|
+
return { distance, blockX: bx, blockY: by, blockZ: bz }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let hit = tryHit(x, y, z)
|
|
89
|
+
if (hit) return hit
|
|
90
|
+
|
|
91
|
+
let t = 0
|
|
92
|
+
while (t <= maxDist) {
|
|
93
|
+
if (tMaxX < tMaxY) {
|
|
94
|
+
if (tMaxX < tMaxZ) {
|
|
95
|
+
x += stepX
|
|
96
|
+
t = tMaxX
|
|
97
|
+
tMaxX += tDeltaX
|
|
98
|
+
} else {
|
|
99
|
+
z += stepZ
|
|
100
|
+
t = tMaxZ
|
|
101
|
+
tMaxZ += tDeltaZ
|
|
102
|
+
}
|
|
103
|
+
} else if (tMaxY < tMaxZ) {
|
|
104
|
+
y += stepY
|
|
105
|
+
t = tMaxY
|
|
106
|
+
tMaxY += tDeltaY
|
|
107
|
+
} else {
|
|
108
|
+
z += stepZ
|
|
109
|
+
t = tMaxZ
|
|
110
|
+
tMaxZ += tDeltaZ
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (t > maxDist) break
|
|
114
|
+
|
|
115
|
+
hit = tryHit(x, y, z)
|
|
116
|
+
if (hit) return hit
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
@@ -33,6 +33,7 @@ import { FireworksManager } from './fireworks'
|
|
|
33
33
|
import { SceneOrigin } from './sceneOrigin'
|
|
34
34
|
import { downloadWorldGeometry } from './worldGeometryExport'
|
|
35
35
|
import { ChunkMeshManager } from './chunkMeshManager'
|
|
36
|
+
import { raycastVoxelSolid } from './thirdPersonVoxelRaycast'
|
|
36
37
|
import type { RendererModuleManifest, RegisteredModule, RendererModuleController } from './rendererModuleSystem'
|
|
37
38
|
import { BUILTIN_MODULES } from './modules/index'
|
|
38
39
|
import { formatPerformanceFactorsDebug, PerformanceMonitor } from '../performanceMonitor'
|
|
@@ -149,7 +150,6 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
149
150
|
private readonly _tpScenePos = new THREE.Vector3()
|
|
150
151
|
private readonly _tpAxisX = new THREE.Vector3(1, 0, 0)
|
|
151
152
|
private readonly _tpAxisY = new THREE.Vector3(0, 1, 0)
|
|
152
|
-
private readonly _tpRaycaster = new THREE.Raycaster()
|
|
153
153
|
private readonly _tpChunkWorldPos = new THREE.Vector3()
|
|
154
154
|
|
|
155
155
|
get tilesRendered() {
|
|
@@ -788,6 +788,10 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
788
788
|
text += `LEG-B: ${formatCompact(s.used)}/${formatCompact(s.capacity)}q `
|
|
789
789
|
}
|
|
790
790
|
text += `MEM: ${this.chunkMeshManager.getEstimatedMemoryUsage().total} `
|
|
791
|
+
const camCollisionBytes = this.cameraCollisionBlockCache.getAllocatedBytes()
|
|
792
|
+
if (camCollisionBytes > 0) {
|
|
793
|
+
text += `CAM: ${formatCompact(camCollisionBytes)}b `
|
|
794
|
+
}
|
|
791
795
|
const pf = formatPerformanceFactorsDebug(this.reactiveState.world.instabilityFactors)
|
|
792
796
|
if (pf) text += `PF: ${pf} `
|
|
793
797
|
// entities can be seen in F3
|
|
@@ -1006,50 +1010,36 @@ export class WorldRendererThree extends WorldRendererCommon {
|
|
|
1006
1010
|
this.debugRaycast(pos, direction, distance)
|
|
1007
1011
|
}
|
|
1008
1012
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (mesh) legacyMeshes.push(mesh)
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
const intersects = raycaster.intersectObjects(legacyMeshes, false)
|
|
1013
|
+
/** Swept-sphere radius for third-person camera collision (baked into voxel AABB expansion). */
|
|
1014
|
+
const CAMERA_COLLISION_RADIUS = 0.25
|
|
1015
|
+
/** Hard floor only guards zero/negative; never clamp up past a hit distance. */
|
|
1016
|
+
const MIN_CAMERA_DISTANCE = 0.05
|
|
1017
|
+
/** Eye→camera minimum so the lens stays outside the player head when a wall squeeze would pull closer. */
|
|
1018
|
+
const MIN_THIRD_PERSON_BODY_CLEARANCE = 0.4
|
|
1019
|
+
|
|
1020
|
+
const blockCache = this.cameraCollisionBlockCache
|
|
1021
|
+
const voxelHit = raycastVoxelSolid(
|
|
1022
|
+
pos.x,
|
|
1023
|
+
pos.y,
|
|
1024
|
+
pos.z,
|
|
1025
|
+
direction.x,
|
|
1026
|
+
direction.y,
|
|
1027
|
+
direction.z,
|
|
1028
|
+
distance,
|
|
1029
|
+
CAMERA_COLLISION_RADIUS,
|
|
1030
|
+
MIN_CAMERA_DISTANCE,
|
|
1031
|
+
(bx, by, bz) => blockCache.isSolidBlock(bx, by, bz)
|
|
1032
|
+
)
|
|
1033
1033
|
|
|
1034
1034
|
let finalDistance = distance
|
|
1035
|
-
if (
|
|
1036
|
-
finalDistance = Math.
|
|
1035
|
+
if (voxelHit !== undefined) {
|
|
1036
|
+
finalDistance = Math.min(finalDistance, voxelHit.distance)
|
|
1037
1037
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const boxHit = this.chunkMeshManager.raycastShaderSectionAABBs(pos, direction, finalDistance, maxCenterDistance)
|
|
1041
|
-
if (boxHit !== undefined) {
|
|
1042
|
-
finalDistance = Math.max(0.5, boxHit - 0.2)
|
|
1038
|
+
if (finalDistance < MIN_THIRD_PERSON_BODY_CLEARANCE) {
|
|
1039
|
+
finalDistance = MIN_THIRD_PERSON_BODY_CLEARANCE
|
|
1043
1040
|
}
|
|
1044
1041
|
|
|
1045
|
-
|
|
1046
|
-
if (legacyGlobalHit !== undefined) {
|
|
1047
|
-
finalDistance = Math.max(0.5, legacyGlobalHit - 0.2)
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
const finalPos = new Vec3(pos.x + direction.x * finalDistance, pos.y + direction.y * finalDistance, pos.z + direction.z * finalDistance)
|
|
1051
|
-
|
|
1052
|
-
return finalPos
|
|
1042
|
+
return new Vec3(pos.x + direction.x * finalDistance, pos.y + direction.y * finalDistance, pos.z + direction.z * finalDistance)
|
|
1053
1043
|
}
|
|
1054
1044
|
|
|
1055
1045
|
private debugRaycastHelper?: THREE.ArrowHelper
|
|
@@ -9,7 +9,7 @@ import blockStatesModels from 'mc-assets/dist/blockStatesModels.json'
|
|
|
9
9
|
import MinecraftData from 'minecraft-data'
|
|
10
10
|
import PrismarineBlockLoader from 'prismarine-block'
|
|
11
11
|
import { Vec3 } from 'vec3'
|
|
12
|
-
import { elemFaces, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 } from '../../mesher-shared/modelsGeometryCommon'
|
|
12
|
+
import { elemFaces, buildElementRotation, buildRotationMatrix, matmul3, matmulmat3, vecadd3, vecsub3 } from '../../mesher-shared/modelsGeometryCommon'
|
|
13
13
|
import type { ExportedWorldGeometry, ExportedSection } from '../../mesher-shared/exportedGeometryTypes'
|
|
14
14
|
import type { MesherGeometryOutput } from '../../mesher-shared/shared'
|
|
15
15
|
import { bakeLegacyVertexColors } from '../../lib/bakeLegacyLight'
|
|
@@ -290,8 +290,7 @@ function getCachedBlockModel(
|
|
|
290
290
|
let localMatrix = null as any
|
|
291
291
|
let localShift = null as any
|
|
292
292
|
if (element.rotation) {
|
|
293
|
-
localMatrix =
|
|
294
|
-
localShift = vecsub3(element.rotation.origin, matmul3(localMatrix, element.rotation.origin))
|
|
293
|
+
;({ localMatrix, localShift } = buildElementRotation(element.rotation))
|
|
295
294
|
}
|
|
296
295
|
return { element, localMatrix, localShift }
|
|
297
296
|
})
|
|
@@ -755,8 +754,7 @@ export function renderWasmOutputToGeometry(
|
|
|
755
754
|
let localMatrix = null as any
|
|
756
755
|
let localShift = null as any
|
|
757
756
|
if (element.rotation) {
|
|
758
|
-
localMatrix =
|
|
759
|
-
localShift = vecsub3(element.rotation.origin, matmul3(localMatrix, element.rotation.origin))
|
|
757
|
+
;({ localMatrix, localShift } = buildElementRotation(element.rotation))
|
|
760
758
|
}
|
|
761
759
|
|
|
762
760
|
// eslint-disable-next-line guard-for-in
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect } from 'vitest'
|
|
3
|
+
import Chunks from 'prismarine-chunk'
|
|
4
|
+
import MinecraftData from 'minecraft-data'
|
|
5
|
+
import { Vec3 } from 'vec3'
|
|
6
|
+
import { CAMERA_COLLISION_BYTES_PER_SECTION, CameraCollisionBlockCache } from '../../three/cameraCollisionBlockCache'
|
|
7
|
+
|
|
8
|
+
const VERSION = '1.16.5'
|
|
9
|
+
|
|
10
|
+
function makeChunkWithStoneFloor(y: number) {
|
|
11
|
+
const mcData = MinecraftData(VERSION)
|
|
12
|
+
const Chunk = Chunks(VERSION) as any
|
|
13
|
+
const chunk = new Chunk(undefined as any)
|
|
14
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
15
|
+
for (let x = 0; x < 16; x++) {
|
|
16
|
+
for (let z = 0; z < 16; z++) {
|
|
17
|
+
chunk.setBlockStateId(new Vec3(x, y, z), stoneId)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return chunk
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('CameraCollisionBlockCache: ingestColumn sets solids and skips air sections', () => {
|
|
24
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
25
|
+
cache.setWorldBounds(0, 256)
|
|
26
|
+
const chunk = makeChunkWithStoneFloor(64)
|
|
27
|
+
cache.ingestColumn(0, 0, chunk.toJson())
|
|
28
|
+
|
|
29
|
+
expect(cache.isSolidBlock(0, 64, 0)).toBe(true)
|
|
30
|
+
expect(cache.isSolidBlock(15, 64, 15)).toBe(true)
|
|
31
|
+
expect(cache.isSolidBlock(0, 65, 0)).toBe(false)
|
|
32
|
+
expect(cache.isSolidBlock(0, 0, 0)).toBe(false)
|
|
33
|
+
|
|
34
|
+
// One 16³ section at y=64 only (not the whole column height).
|
|
35
|
+
expect(cache.getAllocatedSectionCount()).toBe(1)
|
|
36
|
+
expect(cache.getAllocatedBytes()).toBe(CAMERA_COLLISION_BYTES_PER_SECTION)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('CameraCollisionBlockCache: setBlockStateId updates and frees empty sections', () => {
|
|
40
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
41
|
+
cache.setWorldBounds(0, 256)
|
|
42
|
+
const mcData = MinecraftData(VERSION)
|
|
43
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
44
|
+
|
|
45
|
+
cache.setBlockStateId(5, 70, 5, stoneId)
|
|
46
|
+
expect(cache.isSolidBlock(5, 70, 5)).toBe(true)
|
|
47
|
+
expect(cache.getAllocatedSectionCount()).toBe(1)
|
|
48
|
+
|
|
49
|
+
cache.setBlockStateId(5, 70, 5, 0)
|
|
50
|
+
expect(cache.isSolidBlock(5, 70, 5)).toBe(false)
|
|
51
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('CameraCollisionBlockCache: removeColumn frees all section bitsets in column', () => {
|
|
55
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
56
|
+
cache.setWorldBounds(0, 256)
|
|
57
|
+
const mcData = MinecraftData(VERSION)
|
|
58
|
+
const stoneId = mcData.blocksByName.stone.defaultState
|
|
59
|
+
|
|
60
|
+
cache.setBlockStateId(0, 64, 0, stoneId)
|
|
61
|
+
cache.setBlockStateId(0, 80, 0, stoneId)
|
|
62
|
+
expect(cache.getAllocatedSectionCount()).toBe(2)
|
|
63
|
+
|
|
64
|
+
cache.removeColumn(0, 0)
|
|
65
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
66
|
+
expect(cache.isSolidBlock(0, 64, 0)).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('CameraCollisionBlockCache: clear and setWorldBounds drop all sections', () => {
|
|
70
|
+
const cache = new CameraCollisionBlockCache(VERSION)
|
|
71
|
+
cache.setWorldBounds(0, 256)
|
|
72
|
+
const chunk = makeChunkWithStoneFloor(10)
|
|
73
|
+
cache.ingestColumn(16, 32, chunk.toJson())
|
|
74
|
+
expect(cache.getAllocatedSectionCount()).toBeGreaterThan(0)
|
|
75
|
+
|
|
76
|
+
cache.clear()
|
|
77
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
78
|
+
|
|
79
|
+
cache.ingestColumn(16, 32, chunk.toJson())
|
|
80
|
+
cache.setWorldBounds(-64, 320)
|
|
81
|
+
expect(cache.getAllocatedSectionCount()).toBe(0)
|
|
82
|
+
})
|
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
//@ts-nocheck
|
|
2
2
|
import { test, expect } from 'vitest'
|
|
3
|
-
import {
|
|
4
|
-
computeShaderSectionRaycastAabb,
|
|
5
|
-
raycastAabb,
|
|
6
|
-
raycastShaderBlocksAabb,
|
|
7
|
-
raycastSectionAabb,
|
|
8
|
-
sectionAabbIntersectsRay
|
|
9
|
-
} from '../../three/sectionRaycastAabb'
|
|
10
|
-
import { LEGACY_SECTION_HALF_EXTENT } from '../../three/legacySectionCull'
|
|
3
|
+
import { computeShaderSectionRaycastAabb, raycastAabb } from '../../three/sectionRaycastAabb'
|
|
11
4
|
import { SHADER_CUBES_WORDS_PER_FACE } from '../bridge/shaderCubeBridge'
|
|
12
5
|
import { WORD0 } from '../../three/shaders/cubeBlockShader'
|
|
13
6
|
|
|
14
|
-
test('raycastSectionAabb: hit along +X (full 16³)', () => {
|
|
15
|
-
const t = raycastSectionAabb(0, 0, 0, 1, 0, 0, 16, 0, 0, 100)
|
|
16
|
-
expect(t).toBeDefined()
|
|
17
|
-
expect(t!).toBeGreaterThanOrEqual(8)
|
|
18
|
-
expect(t!).toBeLessThanOrEqual(24)
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test('raycastSectionAabb: miss behind ray', () => {
|
|
22
|
-
expect(raycastSectionAabb(0, 0, 0, 1, 0, 0, -32, 0, 0, 100)).toBeUndefined()
|
|
23
|
-
})
|
|
24
|
-
|
|
25
7
|
test('raycastAabb: respects maxDist', () => {
|
|
26
8
|
expect(raycastAabb(0, 0, 0, 1, 0, 0, 100, 0, 0, 108, 0, 8, 10)).toBeUndefined()
|
|
27
9
|
})
|
|
@@ -49,93 +31,6 @@ test('raycastAabb: origin inside box is ignored', () => {
|
|
|
49
31
|
expect(t).toBeUndefined()
|
|
50
32
|
})
|
|
51
33
|
|
|
52
|
-
test('raycastShaderBlocksAabb: hits wall block when origin is inside section aggregate AABB', () => {
|
|
53
|
-
const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE * 3)
|
|
54
|
-
words[0] = 8 | (4 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
|
|
55
|
-
words[4] = 8 | (5 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
|
|
56
|
-
words[8] = 8 | (6 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
|
|
57
|
-
const visitGen = new Uint16Array(4096)
|
|
58
|
-
const visitStamp = 1
|
|
59
|
-
const t = raycastShaderBlocksAabb(words, 0, 3, SHADER_CUBES_WORDS_PER_FACE, 8, 8, 8, 4.5, 5.5, 8.5, 1, 0, 0, 10, visitGen, visitStamp)!
|
|
60
|
-
expect(t).toBeGreaterThan(0)
|
|
61
|
-
expect(t).toBeLessThan(4)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('raycastShaderBlocksAabb: ray into empty space does not hit', () => {
|
|
65
|
-
const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE)
|
|
66
|
-
words[0] = 8 | (4 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
|
|
67
|
-
const visitGen = new Uint16Array(4096)
|
|
68
|
-
const visitStamp = 1
|
|
69
|
-
const t = raycastShaderBlocksAabb(words, 0, 1, SHADER_CUBES_WORDS_PER_FACE, 8, 8, 8, 4.5, 5.5, 8.5, -1, 0, 0, 10, visitGen, visitStamp)
|
|
70
|
-
expect(t).toBeUndefined()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
test('raycastShaderBlocksAabb: eye inside solid block uses exit distance', () => {
|
|
74
|
-
const lx = 5
|
|
75
|
-
const ly = 3
|
|
76
|
-
const lz = 7
|
|
77
|
-
const sectionCenterX = 8
|
|
78
|
-
const sectionCenterY = 8
|
|
79
|
-
const sectionCenterZ = 8
|
|
80
|
-
const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE)
|
|
81
|
-
words[0] = lx | (ly << WORD0.LY_SHIFT) | (lz << WORD0.LZ_SHIFT)
|
|
82
|
-
const visitGen = new Uint16Array(4096)
|
|
83
|
-
const visitStamp = 1
|
|
84
|
-
const ox = sectionCenterX - 8 + lx + 0.5
|
|
85
|
-
const oy = sectionCenterY - 8 + ly + 0.5
|
|
86
|
-
const oz = sectionCenterZ - 8 + lz + 0.5
|
|
87
|
-
const t = raycastShaderBlocksAabb(
|
|
88
|
-
words,
|
|
89
|
-
0,
|
|
90
|
-
1,
|
|
91
|
-
SHADER_CUBES_WORDS_PER_FACE,
|
|
92
|
-
sectionCenterX,
|
|
93
|
-
sectionCenterY,
|
|
94
|
-
sectionCenterZ,
|
|
95
|
-
ox,
|
|
96
|
-
oy,
|
|
97
|
-
oz,
|
|
98
|
-
0,
|
|
99
|
-
0,
|
|
100
|
-
1,
|
|
101
|
-
10,
|
|
102
|
-
visitGen,
|
|
103
|
-
visitStamp
|
|
104
|
-
)!
|
|
105
|
-
expect(t).toBeGreaterThan(0)
|
|
106
|
-
expect(t).toBeLessThanOrEqual(10)
|
|
107
|
-
expect(t).toBeCloseTo(0.5, 5)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('raycastShaderBlocksAabb: SoA stride-1 layout (GlobalBlockBuffer style)', () => {
|
|
111
|
-
const w0 = new Uint32Array(2)
|
|
112
|
-
w0[0] = 8 | (4 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
|
|
113
|
-
w0[1] = 8 | (5 << WORD0.LY_SHIFT) | (8 << WORD0.LZ_SHIFT)
|
|
114
|
-
const visitGen = new Uint16Array(4096)
|
|
115
|
-
const visitStamp = 1
|
|
116
|
-
const t = raycastShaderBlocksAabb(w0, 0, 2, 1, 8, 8, 8, 4.5, 5.5, 8.5, 1, 0, 0, 10, visitGen, visitStamp)!
|
|
117
|
-
expect(t).toBeGreaterThan(0)
|
|
118
|
-
expect(t).toBeLessThan(4)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
const SECTION_HALF = LEGACY_SECTION_HALF_EXTENT + 0.01
|
|
122
|
-
|
|
123
|
-
test('sectionAabbIntersectsRay: ray toward section center within far', () => {
|
|
124
|
-
expect(sectionAabbIntersectsRay(8, 8, 8, 4, 8, 8, 1, 0, 0, 4, SECTION_HALF)).toBe(true)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test('sectionAabbIntersectsRay: far shorter than gap to section', () => {
|
|
128
|
-
expect(sectionAabbIntersectsRay(8, 8, 8, -20, 8, 8, 1, 0, 0, 3, SECTION_HALF)).toBe(false)
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test('sectionAabbIntersectsRay: parallel offset ray misses box', () => {
|
|
132
|
-
expect(sectionAabbIntersectsRay(8, 8, 8, -20, 8, -20, 1, 0, 0, 100, SECTION_HALF)).toBe(false)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
test('sectionAabbIntersectsRay: origin inside box', () => {
|
|
136
|
-
expect(sectionAabbIntersectsRay(8, 8, 8, 8, 8, 8, 0, 1, 0, 4, SECTION_HALF)).toBe(true)
|
|
137
|
-
})
|
|
138
|
-
|
|
139
34
|
test('raycastAabb: narrow floor slab blocks downward ray', () => {
|
|
140
35
|
const words = new Uint32Array(SHADER_CUBES_WORDS_PER_FACE * 2)
|
|
141
36
|
words[0] = 4 | (0 << WORD0.LY_SHIFT) | (4 << WORD0.LZ_SHIFT)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
//@ts-nocheck
|
|
2
|
+
import { test, expect } from 'vitest'
|
|
3
|
+
import { raycastVoxelSolid } from '../../three/thirdPersonVoxelRaycast'
|
|
4
|
+
|
|
5
|
+
const solid = (blocks: Set<string>) => (x: number, y: number, z: number) => blocks.has(`${x},${y},${z}`)
|
|
6
|
+
|
|
7
|
+
test('raycastVoxelSolid: straight-back ray hits wall', () => {
|
|
8
|
+
const blocks = solid(new Set(['5,5,5', '5,5,6', '5,5,7', '5,5,8']))
|
|
9
|
+
const hit = raycastVoxelSolid(0.5, 5.5, 5.5, 1, 0, 0, 10, 0.25, 0.05, blocks)
|
|
10
|
+
expect(hit).toBeDefined()
|
|
11
|
+
expect(hit!.blockX).toBe(5)
|
|
12
|
+
expect(hit!.distance).toBeGreaterThan(0)
|
|
13
|
+
expect(hit!.distance).toBeLessThan(5)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('raycastVoxelSolid: diagonal ray through corner seam still hits volume', () => {
|
|
17
|
+
// Pit walls: solid L-shape; eye above pit looks back-down through corner gap in mesh shell.
|
|
18
|
+
const blocks = solid(
|
|
19
|
+
new Set([
|
|
20
|
+
// floor y=5
|
|
21
|
+
'4,5,4',
|
|
22
|
+
'5,5,4',
|
|
23
|
+
'6,5,4',
|
|
24
|
+
'4,5,5',
|
|
25
|
+
'5,5,5',
|
|
26
|
+
'6,5,5',
|
|
27
|
+
// walls
|
|
28
|
+
'4,6,4',
|
|
29
|
+
'4,7,4',
|
|
30
|
+
'5,6,4',
|
|
31
|
+
'6,6,5',
|
|
32
|
+
'6,7,5',
|
|
33
|
+
// surrounding ground (would be hollow in mesh)
|
|
34
|
+
'3,5,3',
|
|
35
|
+
'3,6,3',
|
|
36
|
+
'3,7,3',
|
|
37
|
+
'7,5,7',
|
|
38
|
+
'7,6,7',
|
|
39
|
+
'7,7,7'
|
|
40
|
+
])
|
|
41
|
+
)
|
|
42
|
+
const dx = -0.6
|
|
43
|
+
const dy = -0.5
|
|
44
|
+
const dz = -0.6
|
|
45
|
+
const len = Math.hypot(dx, dy, dz)
|
|
46
|
+
const hit = raycastVoxelSolid(5.5, 7.5, 5.5, dx / len, dy / len, dz / len, 4, 0.25, 0.05, blocks)
|
|
47
|
+
expect(hit).toBeDefined()
|
|
48
|
+
expect(hit!.distance).toBeLessThan(4)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('raycastVoxelSolid: passes through air and decorative pass-through is caller responsibility', () => {
|
|
52
|
+
const blocks = solid(new Set(['0,0,3']))
|
|
53
|
+
const hit = raycastVoxelSolid(0.5, 0.5, 0.5, 0, 0, 1, 10, 0.25, 0.05, blocks)
|
|
54
|
+
expect(hit?.blockZ).toBe(3)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('raycastVoxelSolid: downward ray hits floor slab', () => {
|
|
58
|
+
const blocks = solid(new Set(['0,4,0']))
|
|
59
|
+
const hit = raycastVoxelSolid(0.5, 6.5, 0.5, 0, -1, 0, 10, 0.25, 0.05, blocks)
|
|
60
|
+
expect(hit).toBeDefined()
|
|
61
|
+
expect(hit!.blockY).toBe(4)
|
|
62
|
+
expect(hit!.distance).toBeCloseTo(1.25, 1)
|
|
63
|
+
})
|