Basic crops system
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
// block models, in the shape of minecraft's block model json: boxes ("elements") with a texture per face.
|
||||
// a block picks one with "model" and fills in its texture variables with "textures". baking turns a model
|
||||
// into quads in block space (0 to 1) that the chunk mesher and the item renderers draw
|
||||
import type { BlockRegistry, BlockTextures } from "./everything_registry.ts";
|
||||
|
||||
export type ModelFace = "top" | "bottom" | "north" | "south" | "west" | "east";
|
||||
export const MODEL_FACES: ModelFace[] = ["top", "bottom", "north", "south", "west", "east"];
|
||||
|
||||
export interface ModelFaceJson {
|
||||
// a texture variable like "#side", or a texture id
|
||||
texture: string;
|
||||
// the part of the texture, [u1, v1, u2, v2] in pixels (0-16). defaults to the part the face covers
|
||||
uv?: [number, number, number, number];
|
||||
// hidden when the neighbor on this side hides faces (a solid block)
|
||||
cullface?: ModelFace;
|
||||
}
|
||||
|
||||
export interface ModelElementJson {
|
||||
// corners of the box in pixels, 0-16 is the block
|
||||
from: [number, number, number];
|
||||
to: [number, number, number];
|
||||
rotation?: {
|
||||
origin: [number, number, number];
|
||||
axis: "x" | "y" | "z";
|
||||
// -45, -22.5, 0, 22.5 or 45
|
||||
angle: number;
|
||||
// stretch the rotated faces back to the block's size, like the cross model does
|
||||
rescale?: boolean;
|
||||
};
|
||||
// directional shading, off for plants so they look the same from every side
|
||||
shade?: boolean;
|
||||
faces: Partial<Record<ModelFace, ModelFaceJson>>;
|
||||
}
|
||||
|
||||
export interface ModelJson {
|
||||
id: string;
|
||||
// defaults for texture variables, can point at other variables ("top": "#side")
|
||||
textures?: Record<string, string>;
|
||||
elements: ModelElementJson[];
|
||||
}
|
||||
|
||||
// a model a block uses, and the textures it fills in
|
||||
export interface BlockVariant {
|
||||
model?: string;
|
||||
textures?: BlockTextures;
|
||||
// turns the model around the vertical axis, clockwise seen from above: 0, 90, 180 or 270
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_MODEL = "engine:cube";
|
||||
|
||||
const cube_face = (texture: string, cullface: ModelFace): ModelFaceJson => ({ texture, cullface });
|
||||
|
||||
// the engine's own models
|
||||
export const BUILTIN_MODELS: Record<string, ModelJson> = {
|
||||
// a full block. "textures" can be one texture, { top, bottom, side } or { front, side }
|
||||
"engine:cube": {
|
||||
id: "engine:cube",
|
||||
textures: { top: "#side", bottom: "#side", front: "#side" },
|
||||
elements: [{
|
||||
from: [0, 0, 0],
|
||||
to: [16, 16, 16],
|
||||
faces: {
|
||||
top: cube_face("#top", "top"),
|
||||
bottom: cube_face("#bottom", "bottom"),
|
||||
north: cube_face("#side", "north"),
|
||||
south: cube_face("#front", "south"),
|
||||
west: cube_face("#side", "west"),
|
||||
east: cube_face("#side", "east"),
|
||||
},
|
||||
}],
|
||||
},
|
||||
// two crossed planes, like flowers and saplings. uses the "cross" texture
|
||||
"engine:cross": {
|
||||
id: "engine:cross",
|
||||
elements: [
|
||||
cross_plane(45),
|
||||
cross_plane(-45),
|
||||
],
|
||||
},
|
||||
// four planes in a # shape, like wheat. uses the "crop" texture
|
||||
"engine:crop": {
|
||||
id: "engine:crop",
|
||||
elements: [
|
||||
{ from: [4, 0, 0], to: [4, 16, 16], shade: false, faces: both_ways("west", "east", "#crop") },
|
||||
{ from: [12, 0, 0], to: [12, 16, 16], shade: false, faces: both_ways("west", "east", "#crop") },
|
||||
{ from: [0, 0, 4], to: [16, 16, 4], shade: false, faces: both_ways("north", "south", "#crop") },
|
||||
{ from: [0, 0, 12], to: [16, 16, 12], shade: false, faces: both_ways("north", "south", "#crop") },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function cross_plane(angle: number): ModelElementJson {
|
||||
return {
|
||||
from: [0.8, 0, 8],
|
||||
to: [15.2, 16, 8],
|
||||
rotation: { origin: [8, 8, 8], axis: "y", angle, rescale: true },
|
||||
shade: false,
|
||||
faces: both_ways("north", "south", "#cross"),
|
||||
};
|
||||
}
|
||||
|
||||
// a flat element seen from both sides, since faces are only drawn from the front
|
||||
function both_ways(a: ModelFace, b: ModelFace, texture: string) {
|
||||
return { [a]: { texture, uv: [0, 0, 16, 16] }, [b]: { texture, uv: [0, 0, 16, 16] } } as Partial<
|
||||
Record<ModelFace, ModelFaceJson>
|
||||
>;
|
||||
}
|
||||
|
||||
export function find_model(id: string, models: Record<string, ModelJson>): ModelJson | undefined {
|
||||
return BUILTIN_MODELS[id] ?? models[id];
|
||||
}
|
||||
|
||||
// what a texture reference means for a block: "#name" looks up the block's textures, then the model's
|
||||
// defaults, and anything else is already a texture id
|
||||
export function resolve_texture(reference: string, textures: BlockTextures | undefined, model?: ModelJson): string {
|
||||
for (let depth = 0; depth < 8 && reference.startsWith("#"); depth++) {
|
||||
const name = reference.slice(1);
|
||||
if (typeof textures === "string") return textures;
|
||||
const next = (textures as Record<string, string> | undefined)?.[name] ?? model?.textures?.[name];
|
||||
if (next === undefined) return "engine:missing";
|
||||
reference = next;
|
||||
}
|
||||
return reference.startsWith("#") ? "engine:missing" : reference;
|
||||
}
|
||||
|
||||
// the block's model and textures for a value with state bits, from the first variant whose conditions match
|
||||
export function block_variant(block: BlockRegistry, states?: Record<string, number>): Required<BlockVariant> {
|
||||
const variant: Required<BlockVariant> = {
|
||||
model: block.model ?? DEFAULT_MODEL,
|
||||
textures: block.textures,
|
||||
y: 0,
|
||||
};
|
||||
if (!block.variants || !states) return variant;
|
||||
for (const [condition, override] of Object.entries(block.variants)) {
|
||||
if (variant_matches(condition, states)) {
|
||||
return {
|
||||
model: override.model ?? variant.model,
|
||||
textures: override.textures ?? variant.textures,
|
||||
y: override.y ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
return variant;
|
||||
}
|
||||
|
||||
// "age=7" or "age=7,facing=2", "" matches everything
|
||||
export function variant_matches(condition: string, states: Record<string, number>) {
|
||||
if (condition === "") return true;
|
||||
return condition.split(",").every((part) => {
|
||||
const [name, value] = part.split("=");
|
||||
return states[name.trim()] === Number(value);
|
||||
});
|
||||
}
|
||||
|
||||
// for inventories and dropped items: blocks with a full cube draw as a cube, anything else as a flat sprite
|
||||
// of its first texture, like minecraft's items for plants
|
||||
export function block_item_texture(block: BlockRegistry): string | undefined {
|
||||
if ((block.model ?? DEFAULT_MODEL) === DEFAULT_MODEL) return undefined;
|
||||
const textures = block.textures;
|
||||
if (typeof textures === "string") return textures;
|
||||
return Object.values(textures)[0] ?? "engine:missing";
|
||||
}
|
||||
|
||||
export function cube_face_texture(block: BlockRegistry, face: "top" | "bottom" | "front" | "side") {
|
||||
return resolve_texture(`#${face}`, block.textures, BUILTIN_MODELS[DEFAULT_MODEL]);
|
||||
}
|
||||
|
||||
// baking
|
||||
|
||||
export interface BakedQuad {
|
||||
// 4 corners counter clockwise seen from the front, x y z in block space
|
||||
positions: number[];
|
||||
// u v per corner, in pixels of the texture (0-16)
|
||||
uvs: number[];
|
||||
texture: string;
|
||||
// the side it faces most, as the mesher's face index (see FACE_INDEX)
|
||||
face: number;
|
||||
// the side whose neighbor can hide it, or -1
|
||||
cull: number;
|
||||
// on the block's edge facing straight out, so it's lit like a full block's face
|
||||
flush: boolean;
|
||||
shade: boolean;
|
||||
}
|
||||
|
||||
// the mesher's face order: top, bottom, front (+z), back (-z), left (-x), right (+x)
|
||||
export const FACE_INDEX: Record<ModelFace, number> = { top: 0, bottom: 1, south: 2, north: 3, west: 4, east: 5 };
|
||||
const FACE_NORMALS = [[0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1], [-1, 0, 0], [1, 0, 0]];
|
||||
// each face's corners in drawing order as which end of the box they're at, same as the mesher
|
||||
export const FACE_CORNERS = [
|
||||
[[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0]],
|
||||
[[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]],
|
||||
[[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]],
|
||||
[[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]],
|
||||
[[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]],
|
||||
[[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]],
|
||||
] as const;
|
||||
// which end of the uv rectangle each corner gets, u then v
|
||||
export const CORNER_UVS = [[0, 1], [1, 1], [1, 0], [0, 0]] as const;
|
||||
// the face a face becomes after turning the model 90 degrees clockwise seen from above
|
||||
const TURN_Y = [0, 1, 4, 5, 3, 2];
|
||||
|
||||
// minecraft's default uvs: the part of the texture the face would cover if the texture was wrapped around the block
|
||||
function default_uv(face: ModelFace, from: number[], to: number[]): [number, number, number, number] {
|
||||
switch (face) {
|
||||
case "top":
|
||||
return [from[0], from[2], to[0], to[2]];
|
||||
case "bottom":
|
||||
return [from[0], 16 - to[2], to[0], 16 - from[2]];
|
||||
case "north":
|
||||
return [16 - to[0], 16 - to[1], 16 - from[0], 16 - from[1]];
|
||||
case "south":
|
||||
return [from[0], 16 - to[1], to[0], 16 - from[1]];
|
||||
case "west":
|
||||
return [from[2], 16 - to[1], to[2], 16 - from[1]];
|
||||
case "east":
|
||||
return [16 - to[2], 16 - to[1], 16 - from[2], 16 - from[1]];
|
||||
}
|
||||
}
|
||||
|
||||
const EPSILON = 1e-4;
|
||||
|
||||
export function bake_model(model: ModelJson, textures: BlockTextures | undefined, y_rotation = 0): BakedQuad[] {
|
||||
const quads: BakedQuad[] = [];
|
||||
const turns = ((Math.round(y_rotation / 90) % 4) + 4) % 4;
|
||||
|
||||
for (const element of model.elements) {
|
||||
const rotate = element_rotation(element);
|
||||
for (const face of MODEL_FACES) {
|
||||
const face_json = element.faces[face];
|
||||
if (!face_json) continue;
|
||||
const index = FACE_INDEX[face];
|
||||
const uv = face_json.uv ?? default_uv(face, element.from, element.to);
|
||||
|
||||
const positions: number[] = [];
|
||||
const uvs: number[] = [];
|
||||
FACE_CORNERS[index].forEach((corner, k) => {
|
||||
let point = corner.map((end, axis) => (end ? element.to[axis] : element.from[axis]) / 16);
|
||||
point = rotate(point);
|
||||
for (let t = 0; t < turns; t++) point = [1 - point[2], point[1], point[0]];
|
||||
positions.push(...point);
|
||||
const [cu, cv] = CORNER_UVS[k];
|
||||
uvs.push(cu ? uv[2] : uv[0], cv ? uv[3] : uv[1]);
|
||||
});
|
||||
|
||||
let cull = face_json.cullface ? FACE_INDEX[face_json.cullface] : -1;
|
||||
for (let t = 0; t < turns && cull >= 0; t++) cull = TURN_Y[cull];
|
||||
const { face: facing, flush } = classify(positions);
|
||||
quads.push({
|
||||
positions,
|
||||
uvs,
|
||||
texture: resolve_texture(face_json.texture, textures, model),
|
||||
face: facing,
|
||||
cull,
|
||||
flush,
|
||||
shade: element.shade ?? true,
|
||||
});
|
||||
}
|
||||
}
|
||||
return quads;
|
||||
}
|
||||
|
||||
// turns a point (in block space) by the element's rotation
|
||||
function element_rotation(element: ModelElementJson): (point: number[]) => number[] {
|
||||
const rotation = element.rotation;
|
||||
if (!rotation || rotation.angle === 0) return (point) => point;
|
||||
|
||||
const axis = { x: 0, y: 1, z: 2 }[rotation.axis];
|
||||
// the two axes that move, in the order that makes a positive angle counter clockwise looking down the axis
|
||||
const [a, b] = axis === 0 ? [1, 2] : axis === 1 ? [2, 0] : [0, 1];
|
||||
const radians = rotation.angle * Math.PI / 180;
|
||||
const cos = Math.cos(radians);
|
||||
const sin = Math.sin(radians);
|
||||
// minecraft's rescale keeps a 45 degree face as wide as the block
|
||||
const scale = rotation.rescale ? 1 / Math.max(Math.abs(cos), Math.abs(sin)) : 1;
|
||||
const origin = rotation.origin.map((v) => v / 16);
|
||||
|
||||
return (point) => {
|
||||
const out = [...point];
|
||||
const da = point[a] - origin[a];
|
||||
const db = point[b] - origin[b];
|
||||
out[a] = origin[a] + (da * cos - db * sin) * scale;
|
||||
out[b] = origin[b] + (da * sin + db * cos) * scale;
|
||||
return out;
|
||||
};
|
||||
}
|
||||
|
||||
// which way a quad faces most, and whether it's flat against the block's edge
|
||||
function classify(p: number[]): { face: number; flush: boolean } {
|
||||
const e1 = [p[3] - p[0], p[4] - p[1], p[5] - p[2]];
|
||||
const e2 = [p[9] - p[0], p[10] - p[1], p[11] - p[2]];
|
||||
const normal = [e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], e1[0] * e2[1] - e1[1] * e2[0]];
|
||||
|
||||
let face = 0;
|
||||
let best = -Infinity;
|
||||
FACE_NORMALS.forEach((n, i) => {
|
||||
const dot = n[0] * normal[0] + n[1] * normal[1] + n[2] * normal[2];
|
||||
if (dot > best) {
|
||||
best = dot;
|
||||
face = i;
|
||||
}
|
||||
});
|
||||
|
||||
const axis = FACE_NORMALS[face].findIndex((v) => v !== 0);
|
||||
const edge = FACE_NORMALS[face][axis] > 0 ? 1 : 0;
|
||||
const flush = [0, 1, 2, 3].every((k) => Math.abs(p[k * 3 + axis] - edge) < EPSILON);
|
||||
return { face, flush };
|
||||
}
|
||||
Reference in New Issue
Block a user