mgba/res/shaders/gbc-lcd.shader/gbc-lcd.fs

422 lines
12 KiB
Forth
Raw Normal View History

/**
* This shader imitates the GameBoy Color subpixel
* arrangement.
*/
varying vec2 texCoord;
uniform sampler2D tex;
uniform vec2 texSize;
/**
* Adds a base color to everything.
* Lower values make black colors darker.
* Higher values make black colors lighter.
* You'll normally want each of these numbers to be close
* to 0, and not normally higher than 1.
*/
uniform vec3 BaseColor;
/**
* Modifies the contrast or saturation of the image.
* Lower values make the image more gray and higher values
* make it more colorful.
* A value of 1 represents a normal, baseline level of
* contrast.
* You'll normally want this to be somewhere around 1.
*/
uniform float SourceContrast;
/**
* Modifies the luminosity of the image.
* Lower values make the image darker and higher values make
* it lighter.
* A value of 1 represents normal, baseline luminosity.
* You'll normally want this to be somewhere around 1.
*/
uniform float SourceLuminosity;
/**
* Lower values look more like a sharp, unshaded image.
* Higher values look more like an LCD display with subpixels.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelBlendAmount;
/**
* Lower values make subpixels darker.
* Higher values make them lighter and over-bright.
* A value of 1 represents a normal, baseline gamma value.
* You'll normally want this to be somewhere around 1.
*/
uniform float SubpixelGamma;
/**
* Higher values allow subpixels to be more blended-together
* and brighter.
* Lower values keep subpixel colors more separated.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelColorBleed;
/**
* Determines the distance between subpixels.
* Lower values put the red, green, and blue subpixels
* within a single pixel closer together.
* Higher values put them farther apart.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelSpread;
/**
* Determines the vertical offset of subpixels within
* a pixel.
* Lower values put the red, green, and blue subpixels
* within a single pixel higher up.
* Higher values put them further down.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelVerticalOffset;
/**
* Lower values make the subpixels horizontally thinner,
* and higher values make them thicker.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelLightWidth;
/**
* Lower values make the subpixels vertically taller,
* and higher values make them shorter.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelLightHeight;
/**
* Lower values make the subpixels sharper and more
* individually distinct.
* Higher values add an increasingly intense glowing
* effect around each subpixel.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelLightGlow;
/**
* Scale the size of pixels up or down.
* Useful for looking at larger than 8x8 subpixel sizes.
* You'll normally want this number to be exactly 1,
* meaning that every group of 3 subpixels corresponds
* to one pixel in the display.
*/
uniform float SubpixelScale;
/**
* GBC subpixels are roughly rectangular shaped, but
* with a rectangular gap in the lower-right corner.
* Lower values make the lower-right gap in each GBC
* subpixel less distinct. A value of 0 results in no
* gap being shown at all.
* Higher values make the gap more distinct.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float SubpixelTabHeight;
/**
* The following three uniforms decide the base colors
* of each of the subpixels.
*
* Default subpixel colors are based on this resource:
* https://gbcc.dev/technology/
* R: #FF7145 (1.00, 0.44, 0.27)
* G: #C1D650 (0.75, 0.84, 0.31)
* B: #3BCEFF (0.23, 0.81, 1.00)
*/
uniform vec3 SubpixelColorRed; // vec3(1.00, 0.38, 0.22);
uniform vec3 SubpixelColorGreen; // vec3(0.60, 0.88, 0.30);
uniform vec3 SubpixelColorBlue; // vec3(0.23, 0.65, 1.00);
/**
* Helper to get luminosity of an RGB color.
* Used with HCL color space related code.
*/
float getColorLumosity(in vec3 rgb) {
return (
(rgb.r * (5.0 / 16.0)) +
(rgb.g * (9.0 / 16.0)) +
(rgb.b * (2.0 / 16.0))
);
}
/**
* Helper to convert RGB color to HCL. (Hue, Chroma, Luma)
*/
vec3 convertRgbToHcl(in vec3 rgb) {
float xMin = min(rgb.r, min(rgb.g, rgb.b));
float xMax = max(rgb.r, max(rgb.g, rgb.b));
float c = xMax - xMin;
float l = getColorLumosity(rgb);
float h = mod((
c == 0 ? 0.0 :
xMax == rgb.r ? ((rgb.g - rgb.b) / c) :
xMax == rgb.g ? ((rgb.b - rgb.r) / c) + 2.0 :
xMax == rgb.b ? ((rgb.r - rgb.g) / c) + 4.0 :
0.0
), 6.0);
return vec3(h, c, l);
}
/**
* Helper to convert HCL color to RGB. (Hue, Chroma, Luma)
*/
vec3 convertHclToRgb(in vec3 hcl) {
vec3 rgb;
float h = mod(hcl.x, 6.0);
float c = hcl.y;
float l = hcl.z;
float x = c * (1.0 - abs(mod(h, 2.0) - 1.0));
if(h <= 1.0) {
rgb = vec3(c, x, 0.0);
}
else if(h <= 2.0) {
rgb = vec3(x, c, 0.0);
}
else if(h <= 3.0) {
rgb = vec3(0.0, c, x);
}
else if(h <= 4.0) {
rgb = vec3(0.0, x, c);
}
else if(h <= 5.0) {
rgb = vec3(x, 0.0, c);
}
else {
rgb = vec3(c, 0.0, x);
}
float lRgb = getColorLumosity(rgb);
float m = l - lRgb;
return clamp(vec3(m, m, m) + rgb, 0.0, 1.0);
}
/**
* Helper to check if a point is contained within
* a rectangular area.
*/
bool getPointInRect(
vec2 point,
vec2 rectTopLeft,
vec2 rectBottomRight
) {
return (
point.x >= rectTopLeft.x &&
point.y >= rectTopLeft.y &&
point.x <= rectBottomRight.x &&
point.y <= rectBottomRight.y
);
}
/**
* Helper to get the nearest offset vector from a
* point to a line segment.
* (The length of this offset vector is the nearest
* distance from the point to the line segment.)
* Thank you to https://stackoverflow.com/a/1501725
*/
vec2 getPointLineDistance(
vec2 point,
vec2 line0,
vec2 line1
) {
vec2 lineDelta = line0 - line1;
float lineLengthSq = dot(lineDelta, lineDelta);
if(lineLengthSq <= 0) {
return line0 - point;
}
float t = (
dot(point - line0, line1 - line0) / lineLengthSq
);
vec2 projection = (
line0 + clamp(t, 0.0, 1.0) * (line1 - line0)
);
return projection - point;
}
/**
* Helper to get the nearest offset vector from a
* point to a rectangle.
* Returns (0, 0) for points within the rectangle.
*/
vec2 getPointRectDistance(
vec2 point,
vec2 rectTopLeft,
vec2 rectBottomRight
) {
if(getPointInRect(point, rectTopLeft, rectBottomRight)) {
return vec2(0.0, 0.0);
}
vec2 rectTopRight = vec2(rectBottomRight.x, rectTopLeft.y);
vec2 rectBottomLeft = vec2(rectTopLeft.x, rectBottomRight.y);
vec2 v0 = getPointLineDistance(point, rectTopLeft, rectTopRight);
vec2 v1 = getPointLineDistance(point, rectBottomLeft, rectBottomRight);
vec2 v2 = getPointLineDistance(point, rectTopLeft, rectBottomLeft);
vec2 v3 = getPointLineDistance(point, rectTopRight, rectBottomRight);
float v0LengthSq = dot(v0, v0);
float v1LengthSq = dot(v1, v1);
float v2LengthSq = dot(v2, v2);
float v3LengthSq = dot(v3, v3);
float minLengthSq = min(
min(v0LengthSq, v1LengthSq),
min(v2LengthSq, v3LengthSq)
);
if(minLengthSq == v0LengthSq) {
return v0;
}
else if(minLengthSq == v1LengthSq) {
return v1;
}
else if(minLengthSq == v2LengthSq) {
return v2;
}
else {
return v3;
}
}
/**
* Helper to get the nearest offset vector from a
* point to a subpixel.
* GBC subpixels are roughly rectangular in shape,
* but have a rectangular gap in their bottom-left
* corner.
* Returns (0, 0) for points within the subpixel.
*/
vec2 getPointSubpixelDistance(
vec2 point,
vec2 subpixelCenter,
vec2 subpixelSizeHalf
) {
float rectLeft = subpixelCenter.x - subpixelSizeHalf.x;
float rectRight = subpixelCenter.x + subpixelSizeHalf.x;
float rectTop = subpixelCenter.y - subpixelSizeHalf.y;
float rectBottom = subpixelCenter.y + subpixelSizeHalf.y;
vec2 offsetLeft = getPointRectDistance(
point,
vec2(rectLeft, rectTop + SubpixelTabHeight),
vec2(subpixelCenter.x, rectBottom)
);
vec2 offsetRight = getPointRectDistance(
point,
vec2(subpixelCenter.x, rectTop),
vec2(rectRight, rectBottom)
);
float offsetLeftLengthSq = dot(offsetLeft, offsetLeft);
float offsetRightLengthSq = dot(offsetRight, offsetRight);
if(offsetLeftLengthSq <= offsetRightLengthSq) {
return offsetLeft;
}
else {
return offsetRight;
}
}
/**
* Helper to get the intensity of light from a
* subpixel.
* The pixelPosition argument represents a
* fragment's position within a pixel.
* Spread represents the subpixel's horizontal
* position within the pixel.
*/
float getSubpixelIntensity(
vec2 pixelPosition,
float spread
) {
vec2 subpixelCenter = vec2(
0.5 + (spread * SubpixelSpread),
1.0 - SubpixelVerticalOffset
);
vec2 subpixelSizeHalf = 0.5 * vec2(
SubpixelLightWidth,
SubpixelLightHeight
);
vec2 offset = getPointSubpixelDistance(
pixelPosition,
subpixelCenter,
subpixelSizeHalf
);
if(SubpixelLightGlow <= 0) {
return dot(offset, offset) <= 0.0 ? 1.0 : 0.0;
}
else {
float dist = length(offset);
float glow = max(0.0,
1.0 - (dist / SubpixelLightGlow)
);
return glow;
}
}
/**
* Helper to apply SubpixelColorBleed to the intensity
* value computed for a fragment and subpixel.
* Subpixel color bleed allows subpixel colors to be
* more strongly coerced to more accurately represent
* the underlying pixel color.
*/
float applySubpixelBleed(
float subpixelIntensity,
float colorSourceChannel
) {
return subpixelIntensity * (
SubpixelColorBleed +
((1.0 - SubpixelColorBleed) * colorSourceChannel)
);
}
void main() {
// Get base color of the pixel, adjust based on
// contrast and luminosity settings.
vec3 colorSource = texture2D(tex, texCoord).rgb;
vec3 colorSourceHcl = convertRgbToHcl(colorSource);
vec3 colorSourceAdjusted = convertHclToRgb(vec3(
colorSourceHcl.x,
colorSourceHcl.y * SourceContrast,
colorSourceHcl.z * SourceLuminosity
));
// Determine how much each subpixel's light should
// affect this fragment.
vec2 pixelPosition = (
mod(texCoord * texSize * SubpixelScale, 1.0)
);
float subpixelIntensityRed = applySubpixelBleed(
getSubpixelIntensity(pixelPosition, -1.0),
colorSourceAdjusted.r
);
float subpixelIntensityGreen = applySubpixelBleed(
getSubpixelIntensity(pixelPosition, +0.0),
colorSourceAdjusted.g
);
float subpixelIntensityBlue = applySubpixelBleed(
getSubpixelIntensity(pixelPosition, +1.0),
colorSourceAdjusted.b
);
vec3 subpixelLightColor = SubpixelGamma * (
(subpixelIntensityRed * SubpixelColorRed) +
(subpixelIntensityGreen * SubpixelColorGreen) +
(subpixelIntensityBlue * SubpixelColorBlue)
);
// Compute final color
vec3 colorResult = clamp(
subpixelLightColor * colorSourceAdjusted, 0.0, 1.0
);
vec3 colorResultBlended = (
((1.0 - SubpixelBlendAmount) * colorSourceAdjusted) +
(SubpixelBlendAmount * colorResult)
);
colorResultBlended = BaseColor + (
(colorResultBlended * (vec3(1.0, 1.0, 1.0) - BaseColor))
);
gl_FragColor = vec4(
colorResultBlended,
1.0
);
}