Files
Emil c7e86663d8
MVP checks / mvp (push) Waiting to run
Expand voxel gameplay, lighting, full-height streaming and world imports
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.
2026-09-17 02:10:53 +03:00

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
);
}