mirror of https://github.com/mgba-emu/mgba.git
422 lines
12 KiB
GLSL
422 lines
12 KiB
GLSL
/**
|
|
* 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
|
|
);
|
|
}
|