2026-08-04 02:09:54 +02:00
|
|
|
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 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
|
2026-08-24 15:17:53 +02:00
|
|
|
/** Beyond this distance (world units), chunks draw their cheaper far groups
|
|
|
|
|
* instead of near geometry. `Infinity` disables LOD. */
|
2026-08-05 09:24:13 +02:00
|
|
|
lodDistance: number
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-04 12:51:07 +02:00
|
|
|
/** Ready-made looks. The demo binds keys 1/2/3 to these, sweeping resolution,
|
|
|
|
|
* color depth, dither, vertex snap, and filtering from crunchy PS1 to clean. */
|
2026-08-04 02:09:54 +02:00
|
|
|
export namespace RenderConfig {
|
2026-08-04 02:39:20 +02:00
|
|
|
export const standard: RenderConfig = {
|
2026-08-08 14:15:32 +02:00
|
|
|
internalWidth: 384,
|
|
|
|
|
internalHeight: 216,
|
2026-08-04 02:09:54 +02:00
|
|
|
upscaleFilter: "nearest",
|
|
|
|
|
colorDepth: 5,
|
|
|
|
|
dither: 1,
|
|
|
|
|
vertexSnap: 1,
|
|
|
|
|
textureFilter: "nearest",
|
|
|
|
|
lighting: "flat",
|
2026-08-04 12:51:07 +02:00
|
|
|
fog: { color: Color.rgb(150, 170, 200), near: 12, far: 200 },
|
2026-08-05 09:24:13 +02:00
|
|
|
lodDistance: 60,
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const ps1: RenderConfig = {
|
|
|
|
|
internalWidth: 320,
|
|
|
|
|
internalHeight: 240,
|
|
|
|
|
upscaleFilter: "nearest",
|
|
|
|
|
colorDepth: 5,
|
|
|
|
|
dither: 1,
|
|
|
|
|
vertexSnap: 1,
|
|
|
|
|
textureFilter: "nearest",
|
|
|
|
|
lighting: "flat",
|
2026-08-04 12:51:07 +02:00
|
|
|
fog: { color: Color.rgb(150, 170, 200), near: 12, far: 200 },
|
2026-08-05 09:24:13 +02:00
|
|
|
lodDistance: 60,
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const soft: RenderConfig = {
|
|
|
|
|
internalWidth: 480,
|
|
|
|
|
internalHeight: 270,
|
|
|
|
|
upscaleFilter: "nearest",
|
|
|
|
|
colorDepth: 6,
|
|
|
|
|
dither: 0.5,
|
|
|
|
|
vertexSnap: 0.5,
|
|
|
|
|
textureFilter: "nearest",
|
|
|
|
|
lighting: "flat",
|
2026-08-04 12:51:07 +02:00
|
|
|
fog: { color: Color.rgb(170, 190, 215), near: 16, far: 240 },
|
2026-08-05 09:24:13 +02:00
|
|
|
lodDistance: 70,
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const clean: RenderConfig = {
|
|
|
|
|
internalWidth: 960,
|
|
|
|
|
internalHeight: 540,
|
|
|
|
|
upscaleFilter: "linear",
|
|
|
|
|
colorDepth: 8,
|
|
|
|
|
dither: 0,
|
|
|
|
|
vertexSnap: 0,
|
|
|
|
|
textureFilter: "linear",
|
|
|
|
|
lighting: "flat",
|
|
|
|
|
fog: null,
|
2026-08-05 09:24:13 +02:00
|
|
|
lodDistance: Infinity,
|
2026-08-04 02:09:54 +02:00
|
|
|
}
|
|
|
|
|
}
|