Files
Emil c7e86663d8
MVP checks / mvp (push) Canceled after 0s
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

190 lines
5.6 KiB
JavaScript

import { key, isFullCube, unitBox } from "./math.js";
import { getBlockFaceTextures, getFaceUV } from "./block-textures.js";
import { createAmbientOcclusionSampler } from "./ambient-occlusion.js";
/** Shared terrain and actor format; all values become Float32 at upload. */
export const MESH_VERTEX_STRIDE = 20;
export const faces = [
{
n: [1, 0, 0],
v: [
[1, 0, 1],
[1, 0, 0],
[1, 1, 0],
[1, 1, 1],
],
},
{
n: [-1, 0, 0],
v: [
[0, 0, 0],
[0, 0, 1],
[0, 1, 1],
[0, 1, 0],
],
},
{
n: [0, 1, 0],
v: [
[0, 1, 1],
[1, 1, 1],
[1, 1, 0],
[0, 1, 0],
],
},
{
n: [0, -1, 0],
v: [
[0, 0, 0],
[1, 0, 0],
[1, 0, 1],
[0, 0, 1],
],
},
{
n: [0, 0, 1],
v: [
[0, 0, 1],
[1, 0, 1],
[1, 1, 1],
[0, 1, 1],
],
},
{
n: [0, 0, -1],
v: [
[1, 0, 0],
[0, 0, 0],
[0, 1, 0],
[1, 1, 0],
],
},
];
export function boxVertices(
out,
pos,
box,
color,
material = 0,
skip = () => false,
yaw = 0,
opacity = 1,
textures = null,
state = "",
ambient = null,
emission = 0,
blockLight = null,
dynamicLight = false,
origin = [0,0,0],
) {
for (let f = 0; f < 6; f++) {
if (skip(faces[f].n)) continue;
const face = faces[f],
c = Math.cos(yaw),
s = Math.sin(yaw),
n = [
face.n[0] * c - face.n[2] * s,
face.n[1],
face.n[0] * s + face.n[2] * c,
];
const light = ambient ? ambient(pos, box, face.n, face.v) : [1, 1, 1, 1];
const propagated = !dynamicLight && blockLight ? blockLight.sampleFace(pos, box, face.n, face.v) : null;
// Flip the diagonal so occlusion interpolation has no dark triangular seam.
const indices = light[0] + light[2] > light[1] + light[3]
? [0, 1, 3, 1, 2, 3] : [0, 1, 2, 0, 2, 3];
for (const i of indices) {
const v = face.v[i].map(
(v, k) => box.min[k] + v * (box.max[k] - box.min[k]),
);
const point = [pos[0] + v[0] * c - v[2] * s, pos[1] + v[1], pos[2] + v[0] * s + v[2] * c];
const local = dynamicLight && blockLight ? blockLight.sample(point.map((p, axis) => p + n[axis] * 0.02)) : null;
out.push(
point[0]-origin[0], point[1]-origin[1], point[2]-origin[2],
...(textures?.[f]?.tint || color),
...n,
material,
opacity,
...getFaceUV(f, v, state, textures?.[f]),
textures?.[f]?.layer ?? -1,
light[i],
emission,
...(propagated?.block[i] || local?.block || [0, 0, 0]),
propagated?.sky[i] ?? local?.sky ?? 1,
);
}
}
}
export function materialType(state = "", effect) {
if (effect === "bounce") return 5;
if (/^minecraft:(water|bubble_column)(\[|$)/.test(state)) return 6;
if (state.includes("grass_block")) return 1;
if (state.includes("planks")) return 2;
if (state.includes("brick")) return 3;
if (/log|stem|wood/.test(state)) return 4;
return 0;
}
/**
* Pure section meshing for both worker and synchronous fallback. World maps and
* the light field are immutable for a build. A caller may reuse faceTextureCache
* between sections; clear it whenever texture layers change.
*/
export function buildSectionMesh({
id, blocks, materials, textureLayers = new Map(), blockLightField = null,
faceTextureCache = new Map(), localCoordinates = false,
}) {
const faceTextures = (mat) => {
if (!textureLayers.size) return null;
if (!faceTextureCache.has(mat.state))
faceTextureCache.set(mat.state, getBlockFaceTextures(mat.state, textureLayers));
return faceTextureCache.get(mat.state);
};
const base = id.split(",").map((value) => Number(value) * 16),
vertices = [], transparent = [],
ambient = createAmbientOcclusionSampler(blocks, materials,
(mat) => !mat || (mat.opacity ?? 1) >= 1 && !faceTextures(mat)?.some(face => face?.cutout));
for (let x = base[0]; x < base[0] + 16; x++)
for (let y = base[1]; y < base[1] + 16; y++)
for (let z = base[2]; z < base[2] + 16; z++) {
const pos = [x, y, z],
block = blocks.getAt ? blocks.getAt(x,y,z) : blocks.get(key(pos));
if (!block) continue;
const mat = materials.get(block) || {
color: [150, 152, 142],
render: [unitBox],
},
color = mat.color.map((v) => v / 255),
textures = faceTextures(mat),
cutout = textures?.some((face) => face?.cutout),
opacity = cutout ? 1 : (mat.opacity ?? 1),
full = isFullCube(mat) && !cutout;
for (const box of mat.render || [unitBox]) {
boxVertices(
opacity < 1 ? transparent : vertices,
pos,
box,
color,
materialType(mat.state, mat.effect),
(n) => {
if (!full) return false;
const neighbor = materials.get(
(blocks.getAt ? blocks.getAt(pos[0]+n[0],pos[1]+n[1],pos[2]+n[2]) : blocks.get(key(pos.map((v, i) => v + n[i])))),
);
return (
isFullCube(neighbor) &&
!faceTextures(neighbor)?.some((face) => face?.cutout)
);
},
0,
opacity,
textures,
mat.state,
ambient,
Math.max(0, Math.min(1, (mat.light || 0) / 15)),
blockLightField, false, localCoordinates ? base : [0,0,0],
);
}
}
return { vertices: new Float32Array(vertices), transparent: new Float32Array(transparent), origin: localCoordinates ? base : [0,0,0] };
}