Add panorama previews and repair beta export rendering

This commit is contained in:
2026-10-04 23:12:34 +05:00
parent 763cf8713c
commit aec0641f87
21 changed files with 802 additions and 60 deletions

35
renderer/block-colors.mjs Normal file
View File

@@ -0,0 +1,35 @@
// Properties such as waterlogged=false must never determine the block's tint.
export function blockTint(state, biome = "minecraft:plains") {
const name = state.split("[")[0].replace(/^minecraft:/, "");
if (name === "water") return biome.includes("swamp") ? "#617b64" : "#3f76e4";
if (name === "spruce_leaves") return "#619961";
if (name === "birch_leaves") return "#80a755";
if (name === "redstone_wire") {
const power = Number(state.match(/power=(\d+)/)?.[1] || 0);
return power ? "#ff3300" : "#4c0000";
}
const leaves = name.endsWith("_leaves") || name === "vine";
if (biome.includes("swamp")) return "#6a7039";
if (/savanna|badlands|desert/.test(biome))
return leaves ? "#aea42a" : "#bfb755";
if (/snow|frozen|taiga/.test(biome)) return leaves ? "#60a17b" : "#80b497";
if (/jungle/.test(biome)) return leaves ? "#30bb0b" : "#59c93c";
return leaves ? "#77ab2f" : "#91bd59";
}
export function firstTextureFrame(width, height, animation) {
if (!animation) return { x: 0, y: 0, width, height };
const w =
animation.width || (animation.height ? width : Math.min(width, height));
const h =
animation.height || (animation.width ? height : Math.min(width, height));
const first = animation.frames?.[0] ?? 0;
const index = typeof first === "number" ? first : first.index;
const columns = Math.floor(width / w);
return {
x: (index % columns) * w,
y: Math.floor(index / columns) * h,
width: w,
height: h,
};
}

View File

@@ -1,5 +1,6 @@
import * as THREE from "/three.js";
import { buildTerrain } from "./terrain-mesh.js";
import { firstTextureFrame } from "./block-colors.mjs";
// This renderer consumes snapshots, never connects to a Minecraft server.
const renderer = new THREE.WebGLRenderer({
antialias: true,
@@ -70,7 +71,39 @@ async function texture(ref) {
ref,
loader
.loadAsync(resourcePath(ref, "textures") + ".png")
.then((t) => {
.then(async (t) => {
// Vanilla animated textures are sprite sheets, not stretched faces.
const metadata =
t.image.width !== t.image.height
? await fetch(resourcePath(ref, "textures") + ".png.mcmeta").then(
(r) => (r.ok ? r.json() : null),
)
: null;
if (metadata?.animation) {
const frame = firstTextureFrame(
t.image.width,
t.image.height,
metadata.animation,
);
const canvas = document.createElement("canvas");
canvas.width = frame.width;
canvas.height = frame.height;
canvas
.getContext("2d")
.drawImage(
t.image,
frame.x,
frame.y,
frame.width,
frame.height,
0,
0,
frame.width,
frame.height,
);
t.image = canvas;
t.needsUpdate = true;
}
t.magFilter = THREE.NearestFilter;
t.minFilter = THREE.NearestFilter;
t.colorSpace = THREE.SRGBColorSpace;

View File

@@ -1,5 +1,6 @@
import * as THREE from "/three.js";
import { OpaqueIndex, QuadBuffer } from "./mesh-buffers.mjs";
import { blockTint } from "./block-colors.mjs";
const nonOpaque =
/air|water|lava|leaves|glass|grass(?!_block)|fern|flower|sapling|torch|fence|wall|slab|stairs|door|pane|rail|vine|carpet/;
@@ -16,6 +17,7 @@ async function compileState(
texture,
materials,
unsupported,
biome,
) {
const faces = [];
for (const { m, v = {} } of variants) {
@@ -40,13 +42,7 @@ async function compileState(
continue;
}
const tint =
f.tintindex !== undefined
? state.includes("water")
? "#5b9eb5"
: state.includes("leaves")
? "#7ba54c"
: "#90b650"
: "#ffffff";
f.tintindex !== undefined ? blockTint(state, biome) : "#ffffff";
const key = ref + "|" + tint;
let material = materials.get(key);
if (!material) {
@@ -60,14 +56,30 @@ async function compileState(
alphaTest: 0.3,
transparent: water,
opacity: water ? 0.8 : 1,
// Grass side overlays share a plane with the dirt face.
polygonOffset: ref?.includes("grass_block_side_overlay"),
polygonOffsetFactor: -1,
polygonOffsetUnits: -1,
}),
};
materials.set(key, material);
}
const normal = new THREE.Vector3(...fd.normal).applyMatrix4(rotation);
if (e.rotation) {
const axis = new THREE.Vector3(
e.rotation.axis === "x" ? 1 : 0,
e.rotation.axis === "y" ? 1 : 0,
e.rotation.axis === "z" ? 1 : 0,
);
normal.copy(
new THREE.Vector3(...fd.normal)
.applyAxisAngle(axis, (e.rotation.angle * Math.PI) / 180)
.applyMatrix4(rotation),
);
}
const positions = new Float64Array(12),
uvs = new Float32Array(8);
const uv = f.uv || [0, 0, 16, 16];
const uv = f.uv || defaultUV(dir, e.from, e.to);
const coords = [
[uv[0] / 16, 1 - uv[3] / 16],
[uv[2] / 16, 1 - uv[3] / 16],
@@ -143,6 +155,17 @@ function fluidModel(state, faceData) {
};
}
function defaultUV(dir, from, to) {
const [x, y, z] = from,
[X, Y, Z] = to;
if (dir === "up") return [x, z, X, Z];
if (dir === "down") return [x, 16 - Z, X, 16 - z];
if (dir === "north") return [16 - X, 16 - Y, 16 - x, 16 - y];
if (dir === "south") return [x, 16 - Y, X, 16 - y];
if (dir === "west") return [z, 16 - Y, Z, 16 - y];
return [16 - Z, 16 - Y, 16 - z, 16 - y];
}
export async function buildTerrain(
snapshot,
scene,
@@ -176,8 +199,14 @@ export async function buildTerrain(
const b = snapshot.blocks[i];
if (b[1] < minY) minY = b[1];
if (b[1] > maxY) maxY = b[1];
if (!states.has(b[3]))
states.set(b[3], { opaque: !nonOpaque.test(b[3]), faces: null });
const key = b[3] + "|" + (b[4] || "minecraft:plains");
if (!states.has(key))
states.set(key, {
state: b[3],
biome: b[4],
opaque: !nonOpaque.test(b[3].split("[")[0]),
faces: null,
});
if (i % 16384 === 0)
await progress(
25 + (2 * i) / snapshot.blocks.length,
@@ -187,7 +216,8 @@ export async function buildTerrain(
const occupancy = new OpaqueIndex(snapshot.bounds, minY, maxY);
for (let i = 0; i < snapshot.blocks.length; i++) {
const b = snapshot.blocks[i];
if (states.get(b[3]).opaque) occupancy.add(b[0], b[1], b[2]);
if (states.get(b[3] + "|" + (b[4] || "minecraft:plains")).opaque)
occupancy.add(b[0], b[1], b[2]);
if (i % 16384 === 0)
await progress(
27 + (3 * i) / snapshot.blocks.length,
@@ -203,7 +233,8 @@ export async function buildTerrain(
for (let i = 0; i < entries.length; i += 8) {
const end = Math.min(i + 8, entries.length);
await Promise.all(
entries.slice(i, end).map(async ([state, entry]) => {
entries.slice(i, end).map(async ([, entry]) => {
const state = entry.state;
const variants = fluid.test(state)
? [fluidModel(state, faceData)]
: await stateModels(state);
@@ -215,6 +246,7 @@ export async function buildTerrain(
texture,
materials,
unsupported,
entry.biome,
);
completedModels++;
}),
@@ -261,7 +293,7 @@ export async function buildTerrain(
x = b[0],
y = b[1],
z = b[2];
const entry = states.get(b[3]);
const entry = states.get(b[3] + "|" + (b[4] || "minecraft:plains"));
const region =
Math.floor(x / 64) -
originX +