import type { Vec2 } from "../math/Vec2" import type { Vec3 } from "../math/Vec3" import type { Texture } from "../render/Texture" import { Camera } from "./Camera" import type { Mesh } from "./Mesh" /** A flat image standing in the world, always turned to face the camera -- * how the PS1 drew most enemies and props instead of 3D models. */ export type Sprite = { /** World anchor at the base (feet) center. */ position: Vec3 /** World-space width and height. */ size: Vec2 texture: Texture } export namespace Sprite { /** * Build the sprite's quad as a Y-axis billboard: it spins around vertical to * face the camera but stays upright, so characters never tilt. Draw the * result with Rasterizer.draw (its alpha cutout hides transparent texels). */ export function billboard(sprite: Sprite, camera: Camera): Mesh { const forward = Camera.forward(camera) // Camera right projected onto the ground plane (== normalize(-fz, 0, fx)). const len = Math.hypot(forward.x, forward.z) || 1 const rx = -forward.z / len const rz = forward.x / len const hw = sprite.size.x / 2 const p = sprite.position const y0 = p.y const y1 = p.y + sprite.size.y const lx = p.x - rx * hw const lz = p.z - rz * hw const gx = p.x + rx * hw const gz = p.z + rz * hw const vertices = [ { pos: { x: lx, y: y0, z: lz }, uv: { x: 0, y: 1 } }, { pos: { x: gx, y: y0, z: gz }, uv: { x: 1, y: 1 } }, { pos: { x: gx, y: y1, z: gz }, uv: { x: 1, y: 0 } }, { pos: { x: lx, y: y1, z: lz }, uv: { x: 0, y: 0 } }, ] return { vertices, indices: [0, 1, 2, 0, 2, 3] } } }