meat/engine/render/Texture.ts
2026-08-04 02:09:54 +02:00

72 lines
2.7 KiB
TypeScript

import { Color } from "./Color"
/** A square-or-rectangular image of packed RGBA texels, row-major. */
export type Texture = {
readonly width: number
readonly height: number
readonly data: Uint32Array
}
export namespace Texture {
/** Generate a `size`x`size` checkerboard of `cells` squares per axis, using
* colors `a` (top-left) and `b`. A stand-in until real textures load. */
export function checker(size: number, cells: number, a: Color, b: Color): Texture {
const data = new Uint32Array(size * size)
const cell = size / cells
for (let y = 0; y < size; y++) {
for (let x = 0; x < size; x++) {
const on = (Math.floor(x / cell) + Math.floor(y / cell)) % 2 === 0
data[y * size + x] = on ? a : b
}
}
return { width: size, height: size, data }
}
/**
* Sample a texel. Coordinates wrap (tile) outside 0..1. `nearest` point-
* samples for crunchy PS1 texels; `linear` bilinearly blends the four
* neighbors for a smooth (non-period) result.
*
* Note there are no mipmaps: when a textured surface is minified in the
* distance, many texels fall inside one pixel and point sampling picks an
* essentially random one, so the pattern aliases into a crawling moire as the
* camera moves. That shimmer is itself part of the PS1 look here; the usual
* cure (mipmaps) is a deliberate future step, not a bug.
*/
export function sample(tex: Texture, u: number, v: number, filter: "nearest" | "linear"): Color {
return filter === "linear" ? bilinear(tex, u, v) : nearest(tex, u, v)
}
function nearest(tex: Texture, u: number, v: number): Color {
const x = wrap(Math.floor(frac(u) * tex.width), tex.width)
const y = wrap(Math.floor(frac(v) * tex.height), tex.height)
return tex.data[y * tex.width + x]
}
function bilinear(tex: Texture, u: number, v: number): Color {
// -0.5 aligns the sample grid to texel centers before blending.
const fx = frac(u) * tex.width - 0.5
const fy = frac(v) * tex.height - 0.5
const x0 = Math.floor(fx)
const y0 = Math.floor(fy)
const tx = fx - x0
const ty = fy - y0
const top = Color.lerp(texel(tex, x0, y0), texel(tex, x0 + 1, y0), tx)
const bottom = Color.lerp(texel(tex, x0, y0 + 1), texel(tex, x0 + 1, y0 + 1), tx)
return Color.lerp(top, bottom, ty)
}
function texel(tex: Texture, x: number, y: number): Color {
return tex.data[wrap(y, tex.height) * tex.width + wrap(x, tex.width)]
}
/** Fractional part in 0..1 (handles negatives), for uv tiling. */
function frac(n: number): number {
return n - Math.floor(n)
}
/** Wrap an index into 0..size-1, staying non-negative for negative inputs. */
function wrap(n: number, size: number): number {
return ((n % size) + size) % size
}
}