142 lines
5.4 KiB
TypeScript
142 lines
5.4 KiB
TypeScript
import { TEXTURE_SIZE } from "$/common/constants.ts";
|
|
import { BlockRegistry, EverythingRegistry, ItemRegistry } from "$/common/everything_registry.ts";
|
|
import { AssetManager } from "$/client/assets.ts";
|
|
import { get_sprite_region } from "$/client/sprites.ts";
|
|
import type { ItemEntity } from "$/client/entity/item_entity.ts";
|
|
import { push_vertex, Texture } from "$/client/renderer/mod.ts";
|
|
|
|
// how big items on the ground are drawn, minecraft's ground transform
|
|
const BLOCK_SCALE = 0.25;
|
|
const ITEM_SCALE = 0.5;
|
|
// keeps texture lookups off the sprite's edge
|
|
const UV_PAD = 0.5;
|
|
|
|
// each face's corners in drawing order (counter clockwise from outside) on a unit cube, with its shade.
|
|
// top, bottom, front, back, left, right, like the chunk mesher
|
|
const CUBE_FACES = [
|
|
{ corners: [[0, 1, 1], [1, 1, 1], [1, 1, 0], [0, 1, 0]], shade: 1.0, texture: "top" },
|
|
{ corners: [[0, 0, 0], [1, 0, 0], [1, 0, 1], [0, 0, 1]], shade: 0.5, texture: "bottom" },
|
|
{ corners: [[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], shade: 0.8, texture: "front" },
|
|
{ corners: [[1, 0, 0], [0, 0, 0], [0, 1, 0], [1, 1, 0]], shade: 0.8, texture: "side" },
|
|
{ corners: [[0, 0, 0], [0, 0, 1], [0, 1, 1], [0, 1, 0]], shade: 0.6, texture: "side" },
|
|
{ corners: [[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]], shade: 0.6, texture: "side" },
|
|
] as const;
|
|
// sprite end each corner gets, u then v
|
|
const CORNER_UVS = [[0, 1], [1, 1], [1, 0], [0, 0]] as const;
|
|
|
|
// where the extra copies of a bigger stack sit, as fractions of the model's size
|
|
const COPY_OFFSETS = [[0, 0, 0], [0.35, 0.2, -0.25], [-0.3, 0.4, 0.2], [0.15, 0.6, 0.35], [-0.2, 0.8, -0.3]];
|
|
|
|
// minecraft draws more copies of the model the bigger the stack is
|
|
function copies(amount: number) {
|
|
if (amount > 48) return 5;
|
|
if (amount > 32) return 4;
|
|
if (amount > 16) return 3;
|
|
if (amount > 1) return 2;
|
|
return 1;
|
|
}
|
|
|
|
// the atlas has to be the current texture
|
|
export function render_item_entity(entity: ItemEntity, partial_tick: number) {
|
|
const atlas = AssetManager.instance.get<Texture>("bworld:textures");
|
|
const item_info = EverythingRegistry.get<ItemRegistry>("items", entity.item.type_id);
|
|
const block_info = item_info?.block_id
|
|
? EverythingRegistry.get<BlockRegistry>("blocks", item_info.block_id)
|
|
: undefined;
|
|
|
|
// spinning and bobbing, like minecraft's ItemEntityRenderer
|
|
const time = entity.age + partial_tick;
|
|
const angle = time / 20 + entity.bob_offset;
|
|
const bob = Math.sin(time / 10 + entity.bob_offset) * 0.1 + 0.1;
|
|
const { x, y, z } = entity.render_position(partial_tick);
|
|
|
|
const scale = block_info ? BLOCK_SCALE : ITEM_SCALE;
|
|
for (let i = 0; i < copies(entity.item.amount); i++) {
|
|
const [ox, oy, oz] = COPY_OFFSETS[i];
|
|
const base = { x: x + ox * scale, y: y + bob + oy * scale * 0.5, z: z + oz * scale };
|
|
if (block_info) {
|
|
push_block(atlas, block_info, base, scale, angle);
|
|
} else {
|
|
push_sprite(atlas, item_texture(entity, item_info), base, scale, angle);
|
|
}
|
|
}
|
|
}
|
|
|
|
// a small cube turned around its middle
|
|
function push_block(
|
|
atlas: Texture,
|
|
block: BlockRegistry,
|
|
base: { x: number; y: number; z: number },
|
|
size: number,
|
|
angle: number,
|
|
) {
|
|
const sin = Math.sin(angle);
|
|
const cos = Math.cos(angle);
|
|
const corner = (cx: number, cy: number, cz: number) => {
|
|
const lx = (cx - 0.5) * size;
|
|
const lz = (cz - 0.5) * size;
|
|
return [base.x + lx * cos - lz * sin, base.y + cy * size, base.z + lx * sin + lz * cos];
|
|
};
|
|
|
|
for (const face of CUBE_FACES) {
|
|
const region = get_sprite_region(block_face_texture(block, face.texture));
|
|
push_textured_quad(atlas, region, face.corners.map(([cx, cy, cz]) => corner(cx, cy, cz)), face.shade);
|
|
}
|
|
}
|
|
|
|
// a flat sprite standing up and turning, drawn from both sides
|
|
function push_sprite(
|
|
atlas: Texture,
|
|
texture_id: string,
|
|
base: { x: number; y: number; z: number },
|
|
size: number,
|
|
angle: number,
|
|
) {
|
|
const region = get_sprite_region(texture_id);
|
|
const dx = Math.cos(angle) * size / 2;
|
|
const dz = Math.sin(angle) * size / 2;
|
|
const bottom = base.y;
|
|
const top = base.y + size;
|
|
const front = [
|
|
[base.x - dx, bottom, base.z - dz],
|
|
[base.x + dx, bottom, base.z + dz],
|
|
[base.x + dx, top, base.z + dz],
|
|
[base.x - dx, top, base.z - dz],
|
|
];
|
|
push_textured_quad(atlas, region, front, 1);
|
|
// the back is the same quad the other way round, mirrored so it isn't drawn backwards
|
|
push_textured_quad(atlas, region, [front[1], front[0], front[3], front[2]], 1);
|
|
}
|
|
|
|
function push_textured_quad(atlas: Texture, region: { x: number; y: number }, corners: number[][], shade: number) {
|
|
const u0 = (region.x * TEXTURE_SIZE + UV_PAD) / atlas.width;
|
|
const v0 = (region.y * TEXTURE_SIZE + UV_PAD) / atlas.height;
|
|
const u1 = ((region.x + 1) * TEXTURE_SIZE - UV_PAD) / atlas.width;
|
|
const v1 = ((region.y + 1) * TEXTURE_SIZE - UV_PAD) / atlas.height;
|
|
|
|
for (const i of [0, 1, 2, 0, 2, 3]) {
|
|
const [px, py, pz] = corners[i];
|
|
const [cu, cv] = CORNER_UVS[i];
|
|
push_vertex(px, py, pz, cu ? u1 : u0, cv ? v1 : v0, shade, shade, shade, 1);
|
|
}
|
|
}
|
|
|
|
function block_face_texture(block: BlockRegistry, face: "top" | "bottom" | "front" | "side") {
|
|
const textures = block.textures;
|
|
if (typeof textures === "string") {
|
|
return textures;
|
|
}
|
|
if ("top" in textures) {
|
|
return face === "top" ? textures.top : face === "bottom" ? textures.bottom : textures.side;
|
|
}
|
|
return face === "front" ? textures.front : textures.side;
|
|
}
|
|
|
|
function item_texture(entity: ItemEntity, item_info: ItemRegistry | undefined) {
|
|
const texture_id = item_info?.texture_id;
|
|
if (typeof texture_id === "function") {
|
|
return texture_id(entity.item);
|
|
}
|
|
return texture_id ?? "engine:missing";
|
|
}
|