Res: Add gbc-lcd shader, imitates GBC LCD subpixel arrangement with an optional backlight effect (#3097)

This commit is contained in:
Sophie Kirschner 2024-01-07 08:54:01 +02:00 committed by GitHub
parent 48253afc54
commit 3becd63ef5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 693 additions and 0 deletions

View File

@ -0,0 +1,119 @@
/**
* This shader creates a backlight bleeding effect,
* and an internal reflection or ghosting effect.
*/
varying vec2 texCoord;
uniform sampler2D tex;
/**
* Determines the color of the backlight bleed.
* Lower values produce less, dimmer light.
* Higher values produce brighter or more colorful light.
* You'll normally want each of these numbers to be close
* to 1, and not normally lower than 0.
*/
uniform vec3 LightColor;
/**
* Affects the shape of the backlight bleed glow.
* Lower values cause the light bleed to fade out quickly
* from the edges.
* Higher values cause the light bleed to fade out more
* softly and gradually toward the center.
* You'll normally want this to be a number from 0 to 1.
*/
uniform float LightSoftness;
/**
* Lower values result in a less visible or intense
* backlight bleed.
* Higher values make the backlight bleed more pronounced.
* You'll normally want this to be a number close to 0,
* and not normally higher than 1.
*/
uniform float LightIntensity;
/**
* Lower values cause the internal reflection or ghosting
* effect to be less visible.
* Higher values cause the effect to be brighter and more
* visible.
* You'll normally want this to be a number close to 0,
* and not normally higher than 1.
*/
uniform float ReflectionBrightness;
/**
* Lower values have the internal reflection or ghosting
* effect appear offset by a lesser distance.
* Higher values have the effect offset by a greater
* distance.
* You'll normally want each of these numbers to be close
* to 0, and not normally higher than 1.
*/
uniform vec2 ReflectionDistance;
#define M_PI 3.1415926535897932384626433832795
/**
* Helper to compute backlight bleed intensity
* for a texCoord input.
*/
float getLightIntensity(vec2 coord) {
vec2 coordCentered = coord - vec2(0.5, 0.5);
float coordDistCenter = (
length(coordCentered) / sqrt(0.5)
);
vec2 coordQuadrant = vec2(
1.0 - (1.5 * min(coord.x, 1.0 - coord.x)),
1.0 - (1.5 * min(coord.y, 1.0 - coord.y))
);
float lightIntensityEdges = (
pow(coordQuadrant.x, 5.0) +
pow(coordQuadrant.y, 5.0)
);
float lightIntensity = (
(1.0 - LightSoftness) * lightIntensityEdges +
LightSoftness * coordDistCenter
);
return clamp(lightIntensity, 0.0, 1.0);
}
/**
* Helper to convert an intensity value into a white
* gray color with that intensity. A radial distortion
* effect with subtle chromatic abberation is applied that
* makes it look a little more like a real old or cheap
* backlight, and also helps to reduce color banding.
*/
vec3 getWhiteVector(float intensity) {
const float DeformAmount = 0.0025;
vec2 texCoordCentered = texCoord - vec2(0.5, 0.5);
float radians = atan(texCoordCentered.y, texCoordCentered.x);
float rot = pow(2.0, 4.0 + floor(6.0 * length(texCoordCentered)));
float deformRed = cos(rot * radians + (2.0 / 3.0 * M_PI));
float deformGreen = cos(rot * radians);
float deformBlue = cos(rot * radians + (4.0 / 3.0 * M_PI));
return clamp(vec3(
intensity + (deformRed * DeformAmount),
intensity + (deformGreen * DeformAmount),
intensity + (deformBlue * DeformAmount)
), 0.0, 1.0);
}
void main() {
vec3 colorSource = texture2D(tex, texCoord).rgb;
vec3 lightWhiteVector = getWhiteVector(getLightIntensity(texCoord));
vec3 colorLight = LightColor * lightWhiteVector;
vec3 colorReflection = texture2D(tex, texCoord - ReflectionDistance).rgb;
vec3 colorResult = (
colorSource +
(colorLight * LightIntensity) +
(colorReflection * ReflectionBrightness)
);
gl_FragColor = vec4(
colorResult,
1.0
);
}

View File

@ -0,0 +1,421 @@
/**
* 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
);
}

View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View File

@ -0,0 +1,129 @@
[shader]
name=gbc-lcd
author=Sophie Kirschner
description=Imitates the GameBoy Color LCD screen subpixel arrangement, with an optional backlight effect.
passes=2
[pass.0]
integerScaling=1
fragmentShader=gbc-lcd.fs
blend=1
[pass.1]
fragmentShader=gbc-lcd-light.fs
[pass.0.uniform.BaseColor]
type=float3
default[0]=0.130
default[1]=0.128
default[2]=0.101
readableName=Screen base color
[pass.0.uniform.SubpixelColorRed]
type=float3
default[0]=1.00
default[1]=0.38
default[2]=0.22
readableName=Red subpixel color
[pass.0.uniform.SubpixelColorGreen]
type=float3
default[0]=0.60
default[1]=0.88
default[2]=0.30
readableName=Green subpixel color
[pass.0.uniform.SubpixelColorBlue]
type=float3
default[0]=0.23
default[1]=0.65
default[2]=1.00
readableName=Blue subpixel color
[pass.0.uniform.SourceContrast]
type=float
default=0.85
readableName=Screen contrast
[pass.0.uniform.SourceLuminosity]
type=float
default=0.88
readableName=Screen luminosity
[pass.0.uniform.SubpixelBlendAmount]
type=float
default=1.0
readableName=Subpixel effect amount
[pass.0.uniform.SubpixelColorBleed]
type=float
default=0.31
readableName=Subpixel color bleeding
[pass.0.uniform.SubpixelSpread]
type=float
default=0.333
readableName=Subpixel spread X
[pass.0.uniform.SubpixelVerticalOffset]
type=float
default=0.48
readableName=Subpixel offset Y
[pass.0.uniform.SubpixelGamma]
type=float
default=1.040
readableName=Subpixel brightness
[pass.0.uniform.SubpixelLightWidth]
type=float
default=0.220
readableName=Subpixel width
[pass.0.uniform.SubpixelLightHeight]
type=float
default=0.850
readableName=Subpixel height
[pass.0.uniform.SubpixelLightGlow]
type=float
default=0.195
readableName=Subpixel glow size
[pass.0.uniform.SubpixelTabHeight]
type=float
default=0.175
readableName=Subpixel tab shaping
[pass.0.uniform.SubpixelScale]
type=float
default=1.0
readableName=Subpixel scale
[pass.1.uniform.LightColor]
type=float3
default[0]=1.000
default[1]=0.968
default[2]=0.882
readableName=Backlight color
[pass.1.uniform.LightIntensity]
type=float
default=0.06
readableName=Backlight intensity
[pass.1.uniform.LightSoftness]
type=float
default=0.67
readableName=Backlight softness
[pass.1.uniform.ReflectionDistance]
type=float2
default[0]=0
default[1]=0.025
readableName=Internal reflection distance
[pass.1.uniform.ReflectionBrightness]
type=float
default=0.032
readableName=Internal reflection brightness