MVP checks / mvp (push) Waiting to run
Add shared Rust/WASM physics, worker meshing and diagnostics, 64-chunk full-height streaming, atlas texture support, and baseline world import. Document the current implementation and include the supplied in-game lobby screenshot.
174 lines
4.7 KiB
JavaScript
174 lines
4.7 KiB
JavaScript
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.opacity ?? 1) >= 1 &&
|
|
!mat.transparent
|
|
);
|
|
}
|