The first commit, the core and all files is adding to repo
This commit is contained in:
396
renderer/scene.js
Normal file
396
renderer/scene.js
Normal file
@@ -0,0 +1,396 @@
|
||||
import * as THREE from "/three.js";
|
||||
// This renderer consumes snapshots, never connects to a Minecraft server.
|
||||
const renderer = new THREE.WebGLRenderer({
|
||||
antialias: true,
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.setPixelRatio(1);
|
||||
document.body.append(renderer.domElement);
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color("#95c4eb");
|
||||
scene.add(new THREE.HemisphereLight("#e9f3ff", "#73704e", 2.0));
|
||||
const sun = new THREE.DirectionalLight("#fff4d6", 2.2);
|
||||
sun.position.set(-40, 90, -30);
|
||||
scene.add(sun);
|
||||
const loader = new THREE.TextureLoader(),
|
||||
jsonCache = new Map(),
|
||||
textureCache = new Map(),
|
||||
resolvedModels = new Map();
|
||||
const resourcePath = (ref, kind) =>
|
||||
`/assets/assets/${ref.includes(":") ? ref.split(":")[0] : "minecraft"}/${kind}/${ref.split(":").at(-1)}`;
|
||||
async function json(path) {
|
||||
if (!jsonCache.has(path))
|
||||
jsonCache.set(
|
||||
path,
|
||||
fetch(path).then((r) => {
|
||||
if (!r.ok) throw new Error(path);
|
||||
return r.json();
|
||||
}),
|
||||
);
|
||||
return jsonCache.get(path);
|
||||
}
|
||||
async function model(ref, chain = []) {
|
||||
if (chain.includes(ref)) throw new Error("Cyclic model");
|
||||
if (resolvedModels.has(ref)) return resolvedModels.get(ref);
|
||||
const raw = await json(resourcePath(ref, "models") + ".json");
|
||||
let parent = {};
|
||||
if (raw.parent && !raw.parent.includes("builtin/"))
|
||||
parent = await model(raw.parent, [...chain, ref]);
|
||||
const merged = {
|
||||
...parent,
|
||||
...raw,
|
||||
textures: { ...parent.textures, ...raw.textures },
|
||||
};
|
||||
resolvedModels.set(ref, merged);
|
||||
return merged;
|
||||
}
|
||||
function textureRef(m, ref) {
|
||||
let i = 0;
|
||||
while (ref?.startsWith("#") && i++ < 20) ref = m.textures[ref.slice(1)];
|
||||
return ref;
|
||||
}
|
||||
async function texture(ref) {
|
||||
if (!ref) return null;
|
||||
if (!textureCache.has(ref)) {
|
||||
const t = await loader.loadAsync(resourcePath(ref, "textures") + ".png");
|
||||
t.magFilter = THREE.NearestFilter;
|
||||
t.minFilter = THREE.NearestFilter;
|
||||
t.colorSpace = THREE.SRGBColorSpace;
|
||||
textureCache.set(ref, t);
|
||||
}
|
||||
return textureCache.get(ref);
|
||||
}
|
||||
function parseState(s) {
|
||||
const [name, raw] = s.split("[");
|
||||
return {
|
||||
name,
|
||||
props: Object.fromEntries(
|
||||
(raw?.replace("]", "") || "")
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((p) => p.split("=")),
|
||||
),
|
||||
};
|
||||
}
|
||||
function match(condition, props) {
|
||||
if (condition.OR) return condition.OR.some((c) => match(c, props));
|
||||
if (condition.AND) return condition.AND.every((c) => match(c, props));
|
||||
return Object.entries(condition).every(([k, v]) =>
|
||||
String(v).split("|").includes(props[k]),
|
||||
);
|
||||
}
|
||||
function selectModels(state, props) {
|
||||
if (state.variants) {
|
||||
for (const [key, v] of Object.entries(state.variants)) {
|
||||
const c = Object.fromEntries(
|
||||
key
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((p) => p.split("=")),
|
||||
);
|
||||
if (match(c, props)) return [Array.isArray(v) ? v[0] : v];
|
||||
}
|
||||
}
|
||||
return (state.multipart || [])
|
||||
.filter((p) => !p.when || match(p.when, props))
|
||||
.map((p) => (Array.isArray(p.apply) ? p.apply[0] : p.apply));
|
||||
}
|
||||
const faceData = {
|
||||
east: {
|
||||
normal: [1, 0, 0],
|
||||
corners: [
|
||||
[1, 0, 1],
|
||||
[1, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 1, 0],
|
||||
],
|
||||
},
|
||||
west: {
|
||||
normal: [-1, 0, 0],
|
||||
corners: [
|
||||
[0, 0, 0],
|
||||
[0, 0, 1],
|
||||
[0, 1, 0],
|
||||
[0, 1, 1],
|
||||
],
|
||||
},
|
||||
up: {
|
||||
normal: [0, 1, 0],
|
||||
corners: [
|
||||
[0, 1, 1],
|
||||
[1, 1, 1],
|
||||
[0, 1, 0],
|
||||
[1, 1, 0],
|
||||
],
|
||||
},
|
||||
down: {
|
||||
normal: [0, -1, 0],
|
||||
corners: [
|
||||
[0, 0, 0],
|
||||
[1, 0, 0],
|
||||
[0, 0, 1],
|
||||
[1, 0, 1],
|
||||
],
|
||||
},
|
||||
south: {
|
||||
normal: [0, 0, 1],
|
||||
corners: [
|
||||
[0, 0, 1],
|
||||
[1, 0, 1],
|
||||
[0, 1, 1],
|
||||
[1, 1, 1],
|
||||
],
|
||||
},
|
||||
north: {
|
||||
normal: [0, 0, -1],
|
||||
corners: [
|
||||
[1, 0, 0],
|
||||
[0, 0, 0],
|
||||
[1, 1, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
},
|
||||
};
|
||||
const nonOpaque = (s) =>
|
||||
/air|water|lava|leaves|glass|grass(?!_block)|fern|flower|sapling|torch|fence|wall|slab|stairs|door|pane|rail|vine|carpet/.test(
|
||||
s,
|
||||
);
|
||||
window.buildTerrain = async (snapshot) => {
|
||||
const started = performance.now(),
|
||||
batches = new Map(),
|
||||
unsupported = new Set();
|
||||
const occupancy = new Map(
|
||||
snapshot.blocks.map((b) => [`${b[0]},${b[1]},${b[2]}`, b[3]]),
|
||||
);
|
||||
const stateCache = new Map();
|
||||
async function stateModels(s) {
|
||||
if (stateCache.has(s)) return stateCache.get(s);
|
||||
const { name, props } = parseState(s);
|
||||
let variants = [];
|
||||
try {
|
||||
const bs = await json(resourcePath(name, "blockstates") + ".json");
|
||||
for (const v of selectModels(bs, props))
|
||||
variants.push({ v, m: await model(v.model) });
|
||||
} catch {
|
||||
unsupported.add(name);
|
||||
}
|
||||
stateCache.set(s, variants);
|
||||
return variants;
|
||||
}
|
||||
// Preload each distinct model once before constructing batched geometry.
|
||||
for (const s of new Set(snapshot.blocks.map((b) => b[3])))
|
||||
await stateModels(s);
|
||||
async function emit(block, m, v = {}) {
|
||||
const [bx, by, bz, s] = block;
|
||||
for (const e of m.elements || []) {
|
||||
const from = e.from.map((n) => n / 16),
|
||||
to = e.to.map((n) => n / 16);
|
||||
const transform = new THREE.Matrix4();
|
||||
const r = new THREE.Euler(
|
||||
(-(v.x || 0) * Math.PI) / 180,
|
||||
(-(v.y || 0) * Math.PI) / 180,
|
||||
0,
|
||||
"YXZ",
|
||||
);
|
||||
transform.makeRotationFromEuler(r);
|
||||
for (const [dir, f] of Object.entries(e.faces)) {
|
||||
const fd = faceData[dir];
|
||||
if (!fd) continue;
|
||||
const n = new THREE.Vector3(...fd.normal).applyMatrix4(transform);
|
||||
const neighbor = occupancy.get(
|
||||
`${bx + Math.round(n.x)},${by + Math.round(n.y)},${bz + Math.round(n.z)}`,
|
||||
);
|
||||
if (f.cullface && neighbor && !nonOpaque(neighbor)) continue;
|
||||
const ref = textureRef(m, f.texture),
|
||||
tex = await texture(ref).catch(() => null);
|
||||
if (!tex) {
|
||||
unsupported.add(s);
|
||||
continue;
|
||||
}
|
||||
const tint =
|
||||
f.tintindex !== undefined
|
||||
? s.includes("water")
|
||||
? "#5b9eb5"
|
||||
: s.includes("leaves")
|
||||
? "#7ba54c"
|
||||
: "#90b650"
|
||||
: "#ffffff";
|
||||
const key = ref + "|" + tint;
|
||||
let batch = batches.get(key);
|
||||
if (!batch) {
|
||||
batch = {
|
||||
positions: [],
|
||||
uvs: [],
|
||||
normals: [],
|
||||
indices: [],
|
||||
tex,
|
||||
tint,
|
||||
};
|
||||
batches.set(key, batch);
|
||||
}
|
||||
const uv = f.uv || [0, 0, 16, 16],
|
||||
coords = [
|
||||
[uv[0] / 16, 1 - uv[3] / 16],
|
||||
[uv[2] / 16, 1 - uv[3] / 16],
|
||||
[uv[0] / 16, 1 - uv[1] / 16],
|
||||
[uv[2] / 16, 1 - uv[1] / 16],
|
||||
];
|
||||
const base = batch.positions.length / 3;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let p = new THREE.Vector3(
|
||||
...fd.corners[i].map((c, j) => from[j] + c * (to[j] - from[j])),
|
||||
);
|
||||
if (e.rotation) {
|
||||
const er = e.rotation,
|
||||
origin = new THREE.Vector3(...er.origin.map((a) => a / 16)),
|
||||
axis = new THREE.Vector3(
|
||||
er.axis === "x" ? 1 : 0,
|
||||
er.axis === "y" ? 1 : 0,
|
||||
er.axis === "z" ? 1 : 0,
|
||||
);
|
||||
p.sub(origin).applyAxisAngle(axis, (er.angle * Math.PI) / 180);
|
||||
if (er.rescale) {
|
||||
const scale = 1 / Math.cos((er.angle * Math.PI) / 180);
|
||||
for (const a of ["x", "y", "z"]) if (a !== er.axis) p[a] *= scale;
|
||||
}
|
||||
p.add(origin);
|
||||
}
|
||||
p.subScalar(0.5)
|
||||
.applyMatrix4(transform)
|
||||
.addScalar(0.5)
|
||||
.add(new THREE.Vector3(bx, by, bz));
|
||||
batch.positions.push(p.x, p.y, p.z);
|
||||
batch.normals.push(n.x, n.y, n.z);
|
||||
let index = i;
|
||||
if (f.rotation) {
|
||||
const order = [0, 1, 3, 2],
|
||||
rank = order.indexOf(i);
|
||||
index = order[(rank + f.rotation / 90) % 4];
|
||||
}
|
||||
batch.uvs.push(...coords[index]);
|
||||
}
|
||||
batch.indices.push(
|
||||
base,
|
||||
base + 1,
|
||||
base + 2,
|
||||
base + 2,
|
||||
base + 1,
|
||||
base + 3,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const b of snapshot.blocks) {
|
||||
if (/minecraft:(water|lava)(\[|$)/.test(b[3])) {
|
||||
await emit(b, {
|
||||
textures: {
|
||||
all:
|
||||
"minecraft:block/" +
|
||||
(b[3].includes("water") ? "water_still" : "lava_still"),
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
from: [0, 0, 0],
|
||||
to: [16, 14, 16],
|
||||
faces: Object.fromEntries(
|
||||
Object.keys(faceData).map((k) => [
|
||||
k,
|
||||
{
|
||||
texture: "#all",
|
||||
tintindex: b[3].includes("water") ? 0 : undefined,
|
||||
cullface: k,
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const { m, v } of await stateModels(b[3])) await emit(b, m, v);
|
||||
}
|
||||
for (const batch of batches.values()) {
|
||||
const g = new THREE.BufferGeometry();
|
||||
g.setAttribute(
|
||||
"position",
|
||||
new THREE.Float32BufferAttribute(batch.positions, 3),
|
||||
);
|
||||
g.setAttribute(
|
||||
"normal",
|
||||
new THREE.Float32BufferAttribute(batch.normals, 3),
|
||||
);
|
||||
g.setAttribute("uv", new THREE.Float32BufferAttribute(batch.uvs, 2));
|
||||
g.setIndex(batch.indices);
|
||||
const mat = new THREE.MeshLambertMaterial({
|
||||
map: batch.tex,
|
||||
color: batch.tint,
|
||||
side: THREE.DoubleSide,
|
||||
alphaTest: 0.3,
|
||||
transparent: batch.tex.image.src.includes("water"),
|
||||
opacity: batch.tex.image.src.includes("water") ? 0.8 : 1,
|
||||
});
|
||||
scene.add(new THREE.Mesh(g, mat));
|
||||
}
|
||||
window.terrainBounds = snapshot.bounds;
|
||||
return {
|
||||
blocks: snapshot.blocks.length,
|
||||
drawCalls: batches.size,
|
||||
unsupported: [...unsupported],
|
||||
buildMs: performance.now() - started,
|
||||
};
|
||||
};
|
||||
const directions = {
|
||||
px: [
|
||||
[1, 0, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
nx: [
|
||||
[-1, 0, 0],
|
||||
[0, 1, 0],
|
||||
],
|
||||
py: [
|
||||
[0, 1, 0],
|
||||
[0, 0, -1],
|
||||
],
|
||||
ny: [
|
||||
[0, -1, 0],
|
||||
[0, 0, 1],
|
||||
],
|
||||
pz: [
|
||||
[0, 0, 1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
nz: [
|
||||
[0, 0, -1],
|
||||
[0, 1, 0],
|
||||
],
|
||||
};
|
||||
window.renderFace = (p, face, size) => {
|
||||
renderer.setSize(size, size);
|
||||
const camera = new THREE.PerspectiveCamera(90, 1, 0.05, 512);
|
||||
camera.position.set(p.x, p.y, p.z);
|
||||
const [dir, up] = directions[face];
|
||||
camera.up.set(...up);
|
||||
camera.lookAt(p.x + dir[0], p.y + dir[1], p.z + dir[2]);
|
||||
renderer.render(scene, camera);
|
||||
return renderer.domElement.toDataURL("image/png");
|
||||
};
|
||||
window.renderMap = (size) => {
|
||||
const b = window.terrainBounds;
|
||||
renderer.setSize(size, size);
|
||||
const camera = new THREE.OrthographicCamera(
|
||||
-(b.maxX - b.minX) / 2,
|
||||
(b.maxX - b.minX) / 2,
|
||||
(b.maxZ - b.minZ) / 2,
|
||||
-(b.maxZ - b.minZ) / 2,
|
||||
0.1,
|
||||
1024,
|
||||
);
|
||||
camera.position.set((b.minX + b.maxX) / 2, 512, (b.minZ + b.maxZ) / 2);
|
||||
camera.up.set(0, 0, -1);
|
||||
camera.lookAt(camera.position.x, 0, camera.position.z);
|
||||
renderer.render(scene, camera);
|
||||
return renderer.domElement.toDataURL("image/png");
|
||||
};
|
||||
window.rendererReady = true;
|
||||
Reference in New Issue
Block a user