UNPKG

@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
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 };