45 lines
1.6 KiB
TypeScript
45 lines
1.6 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
|
|
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] }
|
|
}
|
|
}
|