MVP checks / mvp (push) Canceled after 0s
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.
190 lines
5.6 KiB
JavaScript
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] };
|
|
}
|