Complete local voxel MVP with server, client, MCP, packages and world interchange
This commit is contained in:
+172
@@ -0,0 +1,172 @@
|
||||
export const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
|
||||
export const key = (p) => `${p[0]},${p[1]},${p[2]}`;
|
||||
export const direction = (yaw, pitch) => [
|
||||
Math.sin(yaw) * Math.cos(pitch),
|
||||
Math.sin(pitch),
|
||||
-Math.cos(yaw) * Math.cos(pitch),
|
||||
];
|
||||
export function multiply(a, b) {
|
||||
const o = new Float32Array(16);
|
||||
for (let c = 0; c < 4; c++)
|
||||
for (let r = 0; r < 4; r++)
|
||||
for (let k = 0; k < 4; k++) o[c * 4 + r] += a[k * 4 + r] * b[c * 4 + k];
|
||||
return o;
|
||||
}
|
||||
export function perspective(fovy, aspect, near, far) {
|
||||
const f = 1 / Math.tan(fovy / 2),
|
||||
nf = 1 / (near - far);
|
||||
return new Float32Array([
|
||||
f / aspect,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
f,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
(far + near) * nf,
|
||||
-1,
|
||||
0,
|
||||
0,
|
||||
2 * far * near * nf,
|
||||
0,
|
||||
]);
|
||||
}
|
||||
export function viewMatrix(eye, yaw, pitch) {
|
||||
const d = direction(yaw, pitch),
|
||||
z = d.map((v) => -v),
|
||||
x = [Math.cos(yaw), 0, Math.sin(yaw)],
|
||||
y = [
|
||||
-Math.sin(yaw) * Math.sin(pitch),
|
||||
Math.cos(pitch),
|
||||
Math.cos(yaw) * Math.sin(pitch),
|
||||
];
|
||||
return new Float32Array([
|
||||
x[0],
|
||||
y[0],
|
||||
z[0],
|
||||
0,
|
||||
x[1],
|
||||
y[1],
|
||||
z[1],
|
||||
0,
|
||||
x[2],
|
||||
y[2],
|
||||
z[2],
|
||||
0,
|
||||
-dot(x, eye),
|
||||
-dot(y, eye),
|
||||
-dot(z, eye),
|
||||
1,
|
||||
]);
|
||||
}
|
||||
export function dot(a, b) {
|
||||
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
||||
}
|
||||
export function project(p, m, w, h) {
|
||||
const x = m[0] * p[0] + m[4] * p[1] + m[8] * p[2] + m[12],
|
||||
y = m[1] * p[0] + m[5] * p[1] + m[9] * p[2] + m[13],
|
||||
z = m[2] * p[0] + m[6] * p[1] + m[10] * p[2] + m[14],
|
||||
q = m[3] * p[0] + m[7] * p[1] + m[11] * p[2] + m[15];
|
||||
return q > 0 && z / q < 1
|
||||
? [((x / q + 1) * w) / 2, ((1 - y / q) * h) / 2]
|
||||
: null;
|
||||
}
|
||||
export const unitBox = { min: [0, 0, 0], max: [1, 1, 1] };
|
||||
export function normalizeBoxes(boxes) {
|
||||
return (boxes || [])
|
||||
.map((b) =>
|
||||
Array.isArray(b) ? { min: b.slice(0, 3), max: b.slice(3, 6) } : b,
|
||||
)
|
||||
.filter((b) => b.min?.length === 3 && b.max?.length === 3);
|
||||
}
|
||||
export function rayBox(origin, dir, min, max, reach) {
|
||||
let tmin = -Infinity,
|
||||
tmax = Infinity,
|
||||
normal = [0, 0, 0];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (Math.abs(dir[i]) < 1e-9) {
|
||||
if (origin[i] < min[i] || origin[i] > max[i]) return null;
|
||||
continue;
|
||||
}
|
||||
let a = (min[i] - origin[i]) / dir[i],
|
||||
b = (max[i] - origin[i]) / dir[i],
|
||||
sign = -Math.sign(dir[i]);
|
||||
if (a > b) [a, b] = [b, a];
|
||||
if (a > tmin) {
|
||||
tmin = a;
|
||||
normal = [0, 0, 0];
|
||||
normal[i] = sign;
|
||||
}
|
||||
tmax = Math.min(tmax, b);
|
||||
if (tmin > tmax) return null;
|
||||
}
|
||||
if (tmax < 0 || tmin > reach) return null;
|
||||
return { distance: Math.max(0, tmin), normal };
|
||||
}
|
||||
// A bounded DDA finds candidate cells; exact material boxes determine selection and face.
|
||||
export function raycast(origin, dir, blocks, materials, reach = 6) {
|
||||
const cell = origin.map(Math.floor),
|
||||
step = dir.map(Math.sign);
|
||||
const delta = dir.map((v) => (v === 0 ? Infinity : Math.abs(1 / v)));
|
||||
const next = dir.map((v, i) =>
|
||||
v === 0 ? Infinity : ((v > 0 ? cell[i] + 1 : cell[i]) - origin[i]) / v,
|
||||
);
|
||||
const visited = new Set();
|
||||
let best = null;
|
||||
for (let n = 0; n < 128; n++) {
|
||||
// Official shapes can extend outside their owning cell (e.g. a fence is 1.5 high).
|
||||
// Inspect neighboring owners once, but intersect their exact boxes in world space.
|
||||
for (let x = -3; x <= 3; x++)
|
||||
for (let y = -3; y <= 3; y++)
|
||||
for (let z = -3; z <= 3; z++) {
|
||||
const owner = [cell[0] + x, cell[1] + y, cell[2] + z],
|
||||
idKey = key(owner);
|
||||
if (visited.has(idKey)) continue;
|
||||
visited.add(idKey);
|
||||
const id = blocks.get(idKey);
|
||||
if (!id) continue;
|
||||
const mat = materials.get(id),
|
||||
boxes = mat ? mat.render : [unitBox];
|
||||
for (const box of boxes || [unitBox]) {
|
||||
const hit = rayBox(
|
||||
origin,
|
||||
dir,
|
||||
box.min.map((v, i) => v + owner[i]),
|
||||
box.max.map((v, i) => v + owner[i]),
|
||||
reach,
|
||||
);
|
||||
if (hit && (!best || hit.distance < best.distance))
|
||||
best = { ...hit, pos: owner, block: id };
|
||||
}
|
||||
}
|
||||
const axis =
|
||||
next[0] < next[1]
|
||||
? next[0] < next[2]
|
||||
? 0
|
||||
: 2
|
||||
: next[1] < next[2]
|
||||
? 1
|
||||
: 2;
|
||||
if (
|
||||
next[axis] > reach ||
|
||||
!Number.isFinite(next[axis]) ||
|
||||
(best && next[axis] > best.distance)
|
||||
)
|
||||
break;
|
||||
cell[axis] += step[axis];
|
||||
next[axis] += delta[axis];
|
||||
}
|
||||
return best;
|
||||
}
|
||||
export function isFullCube(mat) {
|
||||
const b = mat?.render;
|
||||
return (
|
||||
b?.length === 1 &&
|
||||
b[0].min.every((v) => v === 0) &&
|
||||
b[0].max.every((v) => v === 1) &&
|
||||
!mat.transparent
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user