Render textured aerial maps and support legacy recovery links

This commit is contained in:
2026-10-05 18:11:12 +05:00
parent dcc3779262
commit 2cbaf45340
12 changed files with 226 additions and 35 deletions

View File

@@ -1,6 +1,7 @@
import * as THREE from "/three.js";
import { buildTerrain } from "./terrain-mesh.js";
import { firstTextureFrame } from "./block-colors.mjs";
import { mapDimensions } from "./map-view.mjs";
// This renderer consumes snapshots, never connects to a Minecraft server.
const renderer = new THREE.WebGLRenderer({
antialias: true,
@@ -21,10 +22,12 @@ window.rendererInfo = {
: "unavailable",
};
scene.background = new THREE.Color("#95c4eb");
scene.add(new THREE.HemisphereLight("#e9f3ff", "#73704e", 2.0));
const ambient = new THREE.HemisphereLight("#e9f3ff", "#73704e", 2.0);
scene.add(ambient);
const sun = new THREE.DirectionalLight("#fff4d6", 2.2);
sun.position.set(-40, 90, -30);
scene.add(sun);
scene.add(sun.target);
const loader = new THREE.TextureLoader(),
jsonCache = new Map(),
textureCache = new Map(),
@@ -142,7 +145,17 @@ function selectModels(state, props) {
.filter(Boolean)
.map((p) => p.split("=")),
);
if (match(c, props)) return [Array.isArray(v) ? v[0] : v];
// Some exports omit default properties (for example snowy=false on a
// grass block). Match every property we do have, so the block remains
// visible instead of silently disappearing from the terrain and map.
if (
Object.entries(c).every(
([name, value]) =>
props[name] === undefined ||
String(value).split("|").includes(props[name]),
)
)
return [Array.isArray(v) ? v[0] : v];
}
}
return (state.multipart || [])
@@ -282,9 +295,10 @@ window.renderFace = (p, face, size) => {
renderScene(camera);
return renderer.domElement.toDataURL("image/webp", 0.85);
};
window.renderMap = (size) => {
window.renderMap = () => {
const b = window.terrainBounds;
renderer.setSize(size, size);
const { width, height } = mapDimensions(b);
renderer.setSize(width, height);
const camera = new THREE.OrthographicCamera(
-(b.maxX - b.minX) / 2,
(b.maxX - b.minX) / 2,
@@ -293,10 +307,60 @@ window.renderMap = (size) => {
0.1,
1024,
);
camera.position.set((b.minX + b.maxX) / 2, 512, (b.minZ + b.maxZ) / 2);
camera.position.set((b.minX + b.maxX) / 2, 320, (b.minZ + b.maxZ) / 2);
camera.up.set(0, 0, -1);
camera.lookAt(camera.position.x, 0, camera.position.z);
renderScene(camera, true);
return renderer.domElement.toDataURL("image/webp", 0.9);
const background = scene.background;
const previousSunPosition = sun.position.clone();
const previousSunTarget = sun.target.position.clone();
const previousAmbient = ambient.intensity;
const previousSunIntensity = sun.intensity;
const centerX = camera.position.x;
const centerZ = camera.position.z;
const radius = Math.hypot(b.maxX - b.minX, b.maxZ - b.minZ) / 2 + 256;
// The panorama renderer keeps its broad, shadowless lighting. For the map,
// an angled sun makes ridges, cliffs and buildings cast readable shadows.
ambient.intensity = 1.25;
sun.intensity = 2.2;
sun.position.set(centerX - 220, 300, centerZ - 180);
sun.target.position.set(centerX, 64, centerZ);
sun.castShadow = true;
sun.shadow.mapSize.set(4096, 4096);
sun.shadow.camera.left = -radius;
sun.shadow.camera.right = radius;
sun.shadow.camera.top = radius;
sun.shadow.camera.bottom = -radius;
sun.shadow.camera.near = 1;
sun.shadow.camera.far = radius * 4;
sun.shadow.camera.updateProjectionMatrix();
sun.shadow.bias = -0.0004;
sun.shadow.normalBias = 0.03;
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap;
for (const mesh of window.terrainMeshes) {
mesh.castShadow = true;
mesh.receiveShadow = true;
}
scene.background = new THREE.Color("#ddc79f");
try {
renderScene(camera, true);
return {
data: renderer.domElement.toDataURL("image/webp", 0.92),
width,
height,
};
} finally {
renderer.shadowMap.enabled = false;
sun.castShadow = false;
for (const mesh of window.terrainMeshes) {
mesh.castShadow = false;
mesh.receiveShadow = false;
}
sun.position.copy(previousSunPosition);
sun.target.position.copy(previousSunTarget);
ambient.intensity = previousAmbient;
sun.intensity = previousSunIntensity;
scene.background = background;
}
};
window.rendererReady = true;