256 lines
6.3 KiB
JavaScript
256 lines
6.3 KiB
JavaScript
import * as THREE from "/three.js";
|
|
import { buildTerrain } from "./terrain-mesh.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();
|
|
const gl = renderer.getContext();
|
|
const gpuInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
window.rendererInfo = {
|
|
vendor: gpuInfo
|
|
? gl.getParameter(gpuInfo.UNMASKED_VENDOR_WEBGL)
|
|
: "unavailable",
|
|
renderer: gpuInfo
|
|
? gl.getParameter(gpuInfo.UNMASKED_RENDERER_WEBGL)
|
|
: "unavailable",
|
|
};
|
|
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)) {
|
|
// Cache the in-flight promise and failures as well as successful textures.
|
|
// Missing vanilla assets must not trigger another request for every face.
|
|
textureCache.set(
|
|
ref,
|
|
loader
|
|
.loadAsync(resourcePath(ref, "textures") + ".png")
|
|
.then((t) => {
|
|
t.magFilter = THREE.NearestFilter;
|
|
t.minFilter = THREE.NearestFilter;
|
|
t.colorSpace = THREE.SRGBColorSpace;
|
|
return t;
|
|
})
|
|
.catch(() => null),
|
|
);
|
|
}
|
|
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],
|
|
],
|
|
},
|
|
};
|
|
window.buildTerrain = async (snapshot) => {
|
|
const unsupported = new Set(),
|
|
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;
|
|
}
|
|
return buildTerrain(
|
|
snapshot,
|
|
scene,
|
|
faceData,
|
|
stateModels,
|
|
textureRef,
|
|
texture,
|
|
unsupported,
|
|
);
|
|
};
|
|
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/webp", 0.85);
|
|
};
|
|
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/webp", 0.9);
|
|
};
|
|
window.rendererReady = true;
|