@mesmotronic/three-retropass
Version:
RetroPass applies a retro aesthetic to your Three.js project, emulating the visual style of classic 8-bit and 16-bit games
386 lines (375 loc) • 11.5 kB
JavaScript
var T = Object.defineProperty;
var C = (e) => {
throw TypeError(e);
};
var P = (e, o, t) => o in e ? T(e, o, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[o] = t;
var v = (e, o, t) => P(e, typeof o != "symbol" ? o + "" : o, t), m = (e, o, t) => o.has(e) || C("Cannot " + t);
var l = (e, o, t) => (m(e, o, "read from private field"), t ? t.call(e) : o.get(e)), h = (e, o, t) => o.has(e) ? C("Cannot add the same private member more than once") : o instanceof WeakSet ? o.add(e) : o.set(e, t), d = (e, o, t, i) => (m(e, o, "write to private field"), i ? i.call(e, t) : o.set(e, t), t);
import * as r from "three";
import { ShaderPass as E } from "three/addons/postprocessing/ShaderPass.js";
function x(e) {
const o = e.length, t = 1, i = new Uint8Array(o * 4);
for (let n = 0; n < e.length; n++) {
const c = e[n];
i[n * 4] = Math.floor(c.r * 255), i[n * 4 + 1] = Math.floor(c.g * 255), i[n * 4 + 2] = Math.floor(c.b * 255), i[n * 4 + 3] = 255;
}
const s = new r.DataTexture(
i,
o,
t,
r.RGBAFormat,
r.UnsignedByteType
);
return s.needsUpdate = !0, s;
}
function p(e) {
const o = [], t = e - 1;
for (let i = 0; i < e; i++)
for (let s = 0; s < e; s++)
for (let n = 0; n < e; n++)
o.push(new r.Color(i / t, s / t, n / t));
return o;
}
function b(e) {
let o = [];
switch (!0) {
// 4096 colours - Full Atari STE / Amiga color palette
case e > 512: {
o = p(16);
break;
}
// 512 colours - Full Atari ST (before E) color palette
case e > 256: {
o = p(8);
break;
}
// 256 colours - Web safe colours plus grayscale
case e > 64: {
const t = p(6);
for (; t.length < 256; ) {
const i = (t.length - 216) / 39;
t.push(new r.Color(i, i, i));
}
o = t;
break;
}
// 64 colours - Web safe colours plus grayscale
case e > 16: {
o = p(4);
break;
}
// 16 colours - Microsoft Windows Standard VGA Palette
case e > 4: {
o = [
new r.Color(0),
// Black
new r.Color(170),
// Blue
new r.Color(43520),
// Green
new r.Color(43690),
// Cyan
new r.Color(11141120),
// Red
new r.Color(11141290),
// Magenta
new r.Color(11162880),
// Brown
new r.Color(11184810),
// Light Gray
new r.Color(5592405),
// Dark Gray
new r.Color(5592575),
// Light Blue
new r.Color(5635925),
// Light Green
new r.Color(5636095),
// Light Cyan
new r.Color(16733525),
// Light Red
new r.Color(16733695),
// Light Magenta
new r.Color(16777045),
// Yellow
new r.Color(16777215)
// White
];
break;
}
// 4 colours - CGA mode 1
case e > 2: {
o = [
new r.Color(0),
// Black
new r.Color(43690),
// Cyan
new r.Color(11141290),
// Magenta
new r.Color(16777215)
// White
];
break;
}
// 2 colours - Monochrome
case e >= 0: {
o = [
new r.Color(0),
// Black
new r.Color(16777215)
// White
];
break;
}
default:
throw new Error(`Invalid colorCount: ${e}`);
}
return o.length !== e && console.warn(`No default color palette found for ${e} colors, using ${o.length} colors instead.`), o;
}
const U = {
uniforms: {
tDiffuse: { value: null },
resolution: { value: new r.Vector2(320, 200) },
colorCount: { value: 16 },
colorTexture: { value: x(b(16)) },
dithering: { value: !0 },
ditheringOffset: { value: 0.2 },
quantizeEnabled: { value: !0 }
},
vertexShader: (
/* glsl */
`
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`
),
fragmentShader: (
/* glsl */
`
uniform sampler2D tDiffuse;
uniform vec2 resolution;
uniform int colorCount;
uniform sampler2D colorTexture;
uniform bool dithering;
uniform float ditheringOffset;
uniform bool quantizeEnabled;
varying vec2 vUv;
// Bayer matrix 4x4
const float bayer4x4[16] = float[16](
0.0 / 16.0, 8.0 / 16.0, 2.0 / 16.0, 10.0 / 16.0,
12.0 / 16.0, 4.0 / 16.0, 14.0 / 16.0, 6.0 / 16.0,
3.0 / 16.0, 11.0 / 16.0, 1.0 / 16.0, 9.0 / 16.0,
15.0 / 16.0, 7.0 / 16.0, 13.0 / 16.0, 5.0 / 16.0
);
// Convert linear color to sRGB to correct brightness
vec3 linearToSrgb(vec3 linearColor) {
vec3 cutoff = step(vec3(0.0031308), linearColor);
vec3 lower = linearColor * 12.92;
vec3 higher = 1.055 * pow(linearColor, vec3(1.0/2.4)) - 0.055;
return mix(lower, higher, cutoff);
}
// Quantize color to palette index for N colors (N = 2..4096)
int quantizeToPaletteIndex(vec3 c, int colorCount) {
// Find the number of steps per channel
int steps = int(pow(float(colorCount), 1.0/3.0) + 0.5);
steps = max(1, steps);
int r = int(clamp(floor(c.r * float(steps - 1) + 0.5), 0.0, float(steps - 1)));
int g = int(clamp(floor(c.g * float(steps - 1) + 0.5), 0.0, float(steps - 1)));
int b = int(clamp(floor(c.b * float(steps - 1) + 0.5), 0.0, float(steps - 1)));
return r * steps * steps + g * steps + b;
}
void main() {
// Compute retro UV for pixelation
vec2 retroUV = (floor(vUv * resolution) + 0.5) / resolution;
vec3 c = texture2D(tDiffuse, retroUV).rgb;
// Compute retro pixel coordinates
vec2 retroCoord = floor(vUv * resolution);
if (dithering) {
float offset = 0.0;
// Skip dithering for pure black pixels
if (!(c.r == 0.0 && c.g == 0.0 && c.b == 0.0)) {
int ix = int(mod(retroCoord.x, 4.0));
int iy = int(mod(retroCoord.y, 4.0));
float bayer = bayer4x4[iy * 4 + ix];
offset = (bayer - 0.5) * ditheringOffset;
}
c += vec3(offset);
c = clamp(c, 0.0, 1.0);
}
vec3 closestColor = vec3(0.0);
// By default we use brute-force for small palettes, quantize for large
if (quantizeEnabled == false || colorCount < 64) {
float minDist = 1e6;
for (int i = 0; i < colorCount; i++) {
vec3 paletteColor = texture2D(colorTexture, vec2((float(i) + 0.5) / float(colorCount), 0.5)).rgb;
float dist = distance(c, paletteColor);
if (dist < minDist) {
minDist = dist;
closestColor = paletteColor;
}
}
} else {
int idx = quantizeToPaletteIndex(c, colorCount);
float paletteIndex = (float(idx) + 0.5) / float(colorCount);
closestColor = texture2D(colorTexture, vec2(paletteIndex, 0.5)).rgb;
}
gl_FragColor = vec4(linearToSrgb(closestColor), 1.0);
}
`
)
}, O = 2, R = 4096;
Array.from({ length: R - O + 1 }).map((e, o) => o + 2);
function w(e) {
return e === r.MathUtils.clamp(e, O, R);
}
var g, a, u, f;
class I extends E {
/**
* Creates a new RetroPass instance
* @param parameters - Configuration parameters for the retro effect
*/
constructor({
colorCount: t = 16,
colorPalette: i,
dithering: s = !0,
ditheringOffset: n = 0.2,
autoDitheringOffset: c = !1,
pixelRatio: D = 0.25,
resolution: z = new r.Vector2(320, 200),
autoResolution: y = !1
} = {}) {
super(U);
v(this, "size", new r.Vector2());
h(this, g);
h(this, a, !1);
h(this, u, !1);
h(this, f, 0);
this.dithering = s, this.ditheringOffset = n, this.autoDitheringOffset = c, this.pixelRatio = D, this.resolution = z, this.autoResolution = y, i ? this.colorPalette = i : this.colorCount = t;
}
/**
* Pixel resolution to use
*/
get resolution() {
return this.uniforms.resolution.value;
}
set resolution(t) {
t.equals(this.uniforms.resolution.value) || this.uniforms.resolution.value.copy(t);
}
/**
* Whether to automatically update the resolution based on the specified pixelRatio
*/
get autoResolution() {
return l(this, u);
}
set autoResolution(t) {
l(this, u) !== t && (d(this, u, t), this.updateResolution());
}
/**
* Pixel ratio to use if autoResolution is true, typically 0.0-1.0 (optional)
*/
get pixelRatio() {
return l(this, f);
}
set pixelRatio(t) {
l(this, f) !== t && (d(this, f, t), this.updateResolution());
}
/**
* The number of colors in the palette
*/
get colorCount() {
return this.uniforms.colorCount.value;
}
set colorCount(t) {
if (t !== this.colorCount) {
if (!w(t))
throw new Error("Invalid colorPalette, must contain between 2 and 4096 colours");
this.quantizeEnabled = !0, this.setColorPalette(b(t));
}
}
/**
* The current color palette
*/
get colorPalette() {
return l(this, g);
}
set colorPalette(t) {
const i = t == null ? void 0 : t.length;
if (!w(i))
throw new Error("Invalid colorPalette, must contain between 2 and 4096 colours");
this.quantizeEnabled = !1, this.setColorPalette(t);
}
/**
* Whether or not to apply dithering
*/
get dithering() {
return this.uniforms.dithering.value;
}
set dithering(t) {
this.uniforms.dithering.value = t;
}
/**
* The amount of dithering to apply, typically 0.0 to 1.0
*/
get ditheringOffset() {
return this.uniforms.ditheringOffset.value;
}
set ditheringOffset(t) {
this.uniforms.ditheringOffset.value = t;
}
/**
* Whether to automatically update the dithering offset based on the color count
*/
get autoDitheringOffset() {
return l(this, a);
}
set autoDitheringOffset(t) {
l(this, a) !== t && (d(this, a, t), t && this.updateDitheringOffset());
}
/**
* Using quantization for larger colour palettes massively improves performance,
* but only supports palettes ordered as a uniform RGB cube and not custom color palettes.
*
* If you want to use a custom color palette over 64 colors, you must set this to false
*
* @deprecated This is now set automatically based on the color palette
* @default true
*/
get quantizeEnabled() {
return this.uniforms.quantizeEnabled.value;
}
set quantizeEnabled(t) {
this.uniforms.quantizeEnabled.value = t;
}
/**
* Set the pixel resolution to use (used by EffectComposer)
* @see {@link RetroPass.resolution}
*/
setSize(t, i) {
this.size.set(t, i), this.updateResolution();
}
/**
* Updates the resolution based on the current pixel ratio and size
*/
updateResolution() {
this.autoResolution && this.resolution.set(this.size.x * this.pixelRatio, this.size.y * this.pixelRatio);
}
/**
* Updates the dithering offset based on the current color count
*/
updateDitheringOffset() {
this.autoDitheringOffset && (this.ditheringOffset = 0.025 + 0.975 / (this.colorCount - 1));
}
setColorPalette(t) {
var n;
const i = t == null ? void 0 : t.length, s = x(t);
this.uniforms.colorCount.value = i, (n = this.uniforms.colorTexture.value) == null || n.dispose(), this.uniforms.colorTexture.value = s, this.autoDitheringOffset && this.updateDitheringOffset(), d(this, g, t.slice());
}
}
g = new WeakMap(), a = new WeakMap(), u = new WeakMap(), f = new WeakMap();
export {
I as RetroPass,
b as createColorPalette,
p as createQuantizedColorPalette
};