diff --git a/res/shaders/gbc-lcd.shader/gbc-lcd-light.fs b/res/shaders/gbc-lcd.shader/gbc-lcd-light.fs new file mode 100644 index 000000000..cb583f4da --- /dev/null +++ b/res/shaders/gbc-lcd.shader/gbc-lcd-light.fs @@ -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 + ); +} diff --git a/res/shaders/gbc-lcd.shader/gbc-lcd.fs b/res/shaders/gbc-lcd.shader/gbc-lcd.fs new file mode 100644 index 000000000..4b88ad3aa --- /dev/null +++ b/res/shaders/gbc-lcd.shader/gbc-lcd.fs @@ -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 + ); +} diff --git a/res/shaders/gbc-lcd.shader/license.txt b/res/shaders/gbc-lcd.shader/license.txt new file mode 100644 index 000000000..68a49daad --- /dev/null +++ b/res/shaders/gbc-lcd.shader/license.txt @@ -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 diff --git a/res/shaders/gbc-lcd.shader/manifest.ini b/res/shaders/gbc-lcd.shader/manifest.ini new file mode 100644 index 000000000..3af7e9e2f --- /dev/null +++ b/res/shaders/gbc-lcd.shader/manifest.ini @@ -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