import { Color } from "./Color" /** Linear distance fog: pixels are untouched at/before `near`, fully `color` * at/after `far`, and blended in between. */ export type Fog = { color: Color near: number far: number } /** * Every PS1-look trait as a live dial. Nothing here is baked into the renderer; * the same scene rendered with two configs gives two eras of hardware, so this * is the object you tweak to experiment. Presets live in the namespace below. */ export type RenderConfig = { /** Internal render resolution before upscaling. The core chunkiness dial: * the whole frame is drawn at this size then scaled up to the display, so * lower numbers mean bigger, blockier pixels. PS1 output was ~320x240. */ internalWidth: number internalHeight: number /** How the low-res buffer is scaled to the screen. `nearest` keeps crisp, * blocky pixels (authentic); `linear` smooths them into a soft blur. */ upscaleFilter: "nearest" | "linear" /** Bits per color channel. The PS1 framebuffer was 15-bit (5 bits each), * which steps smooth gradients into visible bands. 8 = full 24-bit color, * no banding. */ colorDepth: number /** Ordered (Bayer 4x4) dither strength, 0..1. Trades color banding for a * fixed crosshatch of alternating pixels, exactly how the PS1 masked its * 15-bit output. 0 = no dithering. */ dither: number /** Screen-space vertex snap grid in pixels. The PS1 transformed vertices in * low-precision fixed point, so they popped between pixels and models * jittered as the camera moved. 0 = off (smooth), 1 = one-pixel snap, * higher = coarser and more pronounced wobble. */ vertexSnap: number /** * Texture-mapping correction, 0..1. At 0, texture coords interpolate linearly * in screen space (affine): geometrically wrong on any receding surface, so * the texture bends and swims along triangle diagonals -- the classic PS1 * artifact. Faces viewed head-on still look perfect because their depth is * constant. At 1, coords are perspective-correct and everything is straight. * Values in between soften the swim; subdividing geometry reduces it too, * because each smaller triangle spans less depth. */ perspectiveCorrect: number /** Texture sampling. `nearest` point-samples for crunchy PS1 texels; * `linear` does bilinear smoothing (cleaner, but not period-accurate). * Neither uses mipmaps, so distant textures shimmer regardless. */ textureFilter: "nearest" | "linear" /** `flat` gives one directional shade per face (the PS1 used cheap flat / * per-vertex lighting); `none` draws the texture unlit at full brightness. */ lighting: "none" | "flat" /** Distance fog, or null to disable. PS1 games leaned on fog to hide the * short draw distance and the shimmer of far geometry. It also colors pixels * no triangle covers, so the frame's clear color should match `fog.color`. */ fog: Fog | null } /** Ready-made looks. The demo binds keys 1/2/3 to these, and they intentionally * sweep `perspectiveCorrect` 0 -> 0.5 -> 1 so you can watch the texture swim * straighten out as you press through them. */ export namespace RenderConfig { export const psxish: RenderConfig = { internalWidth: 384, internalHeight: 216, upscaleFilter: "nearest", colorDepth: 5, dither: 1, vertexSnap: 1, perspectiveCorrect: 0.25, textureFilter: "nearest", lighting: "flat", fog: { color: Color.rgb(150, 170, 200), near: 6, far: 22 }, } export const ps1: RenderConfig = { internalWidth: 320, internalHeight: 240, upscaleFilter: "nearest", colorDepth: 5, dither: 1, vertexSnap: 1, perspectiveCorrect: 0.25, textureFilter: "nearest", lighting: "flat", fog: { color: Color.rgb(150, 170, 200), near: 6, far: 22 }, } export const soft: RenderConfig = { internalWidth: 480, internalHeight: 270, upscaleFilter: "nearest", colorDepth: 6, dither: 0.5, vertexSnap: 0.5, perspectiveCorrect: 0.5, textureFilter: "nearest", lighting: "flat", fog: { color: Color.rgb(170, 190, 215), near: 10, far: 40 }, } export const clean: RenderConfig = { internalWidth: 960, internalHeight: 540, upscaleFilter: "linear", colorDepth: 8, dither: 0, vertexSnap: 0, perspectiveCorrect: 1, textureFilter: "linear", lighting: "flat", fog: null, } }