Add panorama previews and repair beta export rendering
This commit is contained in:
35
renderer/block-colors.mjs
Normal file
35
renderer/block-colors.mjs
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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 +
|
||||
|
||||
Reference in New Issue
Block a user