@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
340 lines (332 loc) • 9.71 kB
JavaScript
var U = Object.defineProperty;
var C = (e) => {
throw TypeError(e);
};
var T = (e, o, t) => o in e ? U(e, o, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[o] = t;
var p = (e, o, t) => T(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)), c = (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 P } from "three/addons/postprocessing/ShaderPass.js";
function w(e) {
const o = e.length, t = 1, i = new Uint8Array(o * 4);
for (let s = 0; s < e.length; s++) {
const a = e[s];
i[s * 4] = Math.floor(a.r * 255), i[s * 4 + 1] = Math.floor(a.g * 255), i[s * 4 + 2] = Math.floor(a.b * 255), i[s * 4 + 3] = 255;
}
const n = new r.DataTexture(
i,
o,
t,
r.RGBAFormat,
r.UnsignedByteType
);
return n.needsUpdate = !0, n;
}
function x(e) {
let o = [];
switch (!0) {
// 4096 colours - Full Atari STE / Amiga color palette
case e > 512: {
const t = [];
for (let i = 0; i < 16; i++)
for (let n = 0; n < 16; n++)
for (let s = 0; s < 16; s++)
t.push(new r.Color(i / 15, n / 15, s / 15));
o = t;
break;
}
// 512 colours - Full Atari ST (before E) color palette
case e > 256: {
const t = [];
for (let i = 0; i < 8; i++)
for (let n = 0; n < 8; n++)
for (let s = 0; s < 8; s++)
t.push(new r.Color(i / 7, n / 7, s / 7));
o = t;
break;
}
// 256 colours - Web safe palette plus grayscale
case e > 16: {
const t = [], i = [0, 0.2, 0.4, 0.6, 0.8, 1];
for (let n of i)
for (let s of i)
for (let a of i)
t.push(new r.Color(n, s, a));
for (let n = 0; n < 40; n++) {
const s = n / 39;
t.push(new r.Color(s, s, s));
}
o = t.slice(0, 256);
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 M = {
uniforms: {
tDiffuse: { value: null },
resolution: { value: new r.Vector2(320, 200) },
colorCount: { value: 16 },
colorTexture: { value: w(x(16)) },
dithering: { value: !0 },
ditheringOffset: { value: 0.2 }
},
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;
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
);
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);
}
// Find closest color in palette
vec3 closestColor = vec3(0.0);
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;
}
}
gl_FragColor = vec4(closestColor, 1.0);
}
`
)
}, O = 2, b = 4096;
Array.from({ length: b - O + 1 }).map((e, o) => o + 2);
function v(e) {
return e === r.MathUtils.clamp(e, O, b);
}
var g, f, u, h;
class _ extends P {
/**
* Creates a new RetroPass instance
* @param parameters - Configuration parameters for the retro effect
*/
constructor({
colorCount: t = 16,
colorPalette: i,
dithering: n = !0,
ditheringOffset: s = 0.2,
autoDitheringOffset: a = !1,
pixelRatio: R = 0.25,
resolution: D = new r.Vector2(320, 200),
autoResolution: y = !1
} = {}) {
super(M);
p(this, "size", new r.Vector2());
c(this, g);
c(this, f, !1);
c(this, u, !1);
c(this, h, 0);
this.dithering = n, this.ditheringOffset = s, this.autoDitheringOffset = a, this.pixelRatio = R, this.resolution = D, 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, h);
}
set pixelRatio(t) {
l(this, h) !== t && (d(this, h, 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 (!v(t))
throw new Error("Invalid colorPalette, must contain between 2 and 4096 colours");
this.colorPalette = x(t);
}
}
/**
* The current color palette
*/
get colorPalette() {
return l(this, g);
}
set colorPalette(t) {
var s;
const i = t == null ? void 0 : t.length;
if (!v(i))
throw new Error("Invalid colorPalette, must contain between 2 and 4096 colours");
const n = w(t);
this.uniforms.colorCount.value = i, (s = this.uniforms.colorTexture.value) == null || s.dispose(), this.uniforms.colorTexture.value = n, this.autoDitheringOffset && this.updateDitheringOffset(), d(this, g, t.slice());
}
/**
* 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, f);
}
set autoDitheringOffset(t) {
l(this, f) !== t && (d(this, f, t), t && this.updateDitheringOffset());
}
/**
* 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(Math.round(this.size.x * this.pixelRatio), Math.round(this.size.y * this.pixelRatio));
}
/**
* Updates the dithering offset based on the current color count
*/
updateDitheringOffset() {
this.autoDitheringOffset && (this.ditheringOffset = 0.1 + 0.9 / (this.colorCount - 1));
}
}
g = new WeakMap(), f = new WeakMap(), u = new WeakMap(), h = new WeakMap();
export {
_ as RetroPass,
x as createColorPalette
};