72 lines
2.7 KiB
TypeScript
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
|
|
}
|
|
}
|