@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
105 lines (92 loc) • 3.4 kB
JavaScript
import * as THREE from "three";
import { createColorTexture } from "../../utils/createColorTexture";
import { createColorPalette } from "../../utils/createColorPalette";
/**
* Shader that creates retro-style post-processing effect
*/
export const RetroShader = {
uniforms: {
tDiffuse: { value: null },
uResolution: { value: new THREE.Vector2(320, 200) },
uColorCount: { value: 16 },
uColorTexture: { value: createColorTexture(createColorPalette(16)) },
uDithering: { value: true },
uDitheringOffset: { value: 0.2 },
uIsQuantized: { value: true },
uInverted: { value: false },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform sampler2D tDiffuse;
uniform vec2 uResolution;
uniform int uColorCount;
uniform sampler2D uColorTexture;
uniform bool uDithering;
uniform float uDitheringOffset;
uniform bool uIsQuantized;
uniform bool uInverted;
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
);
// Optimized: Directly quantize to the nearest cube color, no brute-force search
vec3 quantizeToNearestCubeColor(vec3 c, int colorCount) {
// Find largest N such that N^3 <= colorCount
float stepsF = floor(pow(float(colorCount), 1.0/3.0));
float maxIdx = stepsF - 1.0;
// Quantize each channel to nearest step
float r = floor(c.r * maxIdx + 0.5) / maxIdx;
float g = floor(c.g * maxIdx + 0.5) / maxIdx;
float b = floor(c.b * maxIdx + 0.5) / maxIdx;
return vec3(r, g, b);
}
void main() {
// Compute retro UV for pixelation
vec2 retroUV = (floor(vUv * uResolution) + 0.5) / uResolution;
vec3 c = texture2D(tDiffuse, retroUV).rgb;
if (uInverted) {
c = 1.0 - c;
}
// Compute retro pixel coordinates
vec2 retroCoord = floor(vUv * uResolution);
if (uDithering) {
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) * uDitheringOffset;
}
c += vec3(offset);
c = clamp(c, 0.0, 1.0);
}
vec3 closestColor = vec3(0.0);
// By default we use brute-force small palettes, quantize large
if (uIsQuantized == false || uColorCount < 27) {
float minDist = 1e6;
for (int i = 0; i < uColorCount; i++) {
vec3 paletteColor = texture2D(uColorTexture, vec2((float(i) + 0.5) / float(uColorCount), 0.5)).rgb;
float dist = distance(c, paletteColor);
if (dist < minDist) {
minDist = dist;
closestColor = paletteColor;
}
}
} else {
closestColor = quantizeToNearestCubeColor(c, uColorCount);
}
gl_FragColor = vec4(closestColor, 1.0);
}
`,
};