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 } }