meat/engine/scene/Sprite.ts
2026-08-04 19:12:09 +02:00

42 lines
1.5 KiB
TypeScript

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
// Flat verts (x, y, z, u, v) per corner: bottom-left, bottom-right, top-right,
// top-left.
const verts = [lx, y0, lz, 0, 1, gx, y0, gz, 1, 1, gx, y1, gz, 1, 0, lx, y1, lz, 0, 0]
return { verts, indices: [0, 1, 2, 0, 2, 3] }
}
}