feat: 1995
This commit is contained in:
commit
fb89263930
69 changed files with 3359 additions and 0 deletions
75
engine/render/Framebuffer.ts
Normal file
75
engine/render/Framebuffer.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Color } from "./Color"
|
||||
import type { RenderConfig } from "./RenderConfig"
|
||||
|
||||
/** CPU color + depth buffer the renderer writes into before it is blitted to a
|
||||
* canvas. Kept as flat typed arrays so it needs no DOM and can also run
|
||||
* headless (server-side rendering, tests, baking). */
|
||||
export type Framebuffer = {
|
||||
readonly width: number
|
||||
readonly height: number
|
||||
/** Packed RGBA pixels; see Color. Aliased as an ImageData at blit time. */
|
||||
readonly color: Uint32Array
|
||||
/** Per-pixel depth stored as 1/w. 1/w (unlike w) interpolates linearly in
|
||||
* screen space, so it is both cheap and correct to compare. Larger = nearer;
|
||||
* cleared to 0 = infinitely far. */
|
||||
readonly depth: Float32Array
|
||||
}
|
||||
|
||||
export namespace Framebuffer {
|
||||
export function create(width: number, height: number): Framebuffer {
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
color: new Uint32Array(width * height),
|
||||
depth: new Float32Array(width * height),
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset every pixel to `color` and depth to 0 (farthest). Call once a frame
|
||||
* before drawing; `color` should match the fog color so uncovered pixels
|
||||
* (gaps past the geometry) blend seamlessly. */
|
||||
export function clear(fb: Framebuffer, color: Color): void {
|
||||
fb.color.fill(color)
|
||||
fb.depth.fill(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Posterize the color buffer to `config.colorDepth` bits per channel with a
|
||||
* Bayer 4x4 ordered dither, in place. This is a post-process over the whole
|
||||
* frame (run after all geometry), reproducing the PS1's banded-yet-dithered
|
||||
* 15-bit output. Skipped entirely when it would be a no-op (full depth, no
|
||||
* dither).
|
||||
*/
|
||||
export function quantize(fb: Framebuffer, config: RenderConfig): void {
|
||||
const levels = (1 << config.colorDepth) - 1
|
||||
if (levels >= 255 && config.dither === 0) {
|
||||
return
|
||||
}
|
||||
const { width, height, color } = fb
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
// Per-pixel threshold from the tiled Bayer matrix, centered on 0 and
|
||||
// scaled by strength, nudges each channel before it snaps to a level.
|
||||
const threshold = (BAYER4[(y & 3) * 4 + (x & 3)] / 16 - 0.5) * config.dither
|
||||
const i = y * width + x
|
||||
const c = color[i]
|
||||
color[i] = Color.rgb(
|
||||
channel(Color.r(c), threshold, levels),
|
||||
channel(Color.g(c), threshold, levels),
|
||||
channel(Color.b(c), threshold, levels),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Bayer 4x4 threshold map (values 0..15), read modulo 4 in x and y. */
|
||||
const BAYER4 = [0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5]
|
||||
|
||||
/** Snap one 0..255 channel to `levels` steps after applying the dither
|
||||
* threshold, then expand back to 0..255. */
|
||||
function channel(value: number, threshold: number, levels: number): number {
|
||||
const n = value / 255 + threshold / levels
|
||||
const q = Math.min(levels, Math.max(0, Math.round(n * levels)))
|
||||
return Math.round((q / levels) * 255)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue