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

105 lines (92 loc) 3.4 kB
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); } `, };