Files
Funkin/source/funkin/graphics/shaders/DropShadowShader.hx
T
2026-05-04 04:12:59 -06:00

508 lines
13 KiB
Haxe

package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
import funkin.graphics.FunkinSprite;
import flixel.math.FlxAngle;
import flixel.graphics.frames.FlxFrame;
import openfl.display.BitmapData;
/**
* A shader that aims to *mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
*
* Has options for color, angle, distance, and a threshold to not cast the shadow on parts like outlines.
* Can also be supplied a secondary mask which can then have an alternate threshold, for when sprites have too many conflicting colors
* for the drop shadow to look right (e.g. the tankmen on GF's speakers).
*
* Also has an Adjust Color shader in here so they can work together when needed.
*/
class DropShadowShader extends FlxShader
{
/**
* The color of the drop shadow.
*/
public var color(default, set):FlxColor;
/**
* The angle of the drop shadow.
*
* For reference, depending on the angle, the affected side will be:
* 0 = RIGHT
* 90 = UP
* 180 = LEFT
* 270 = DOWN
*/
public var angle(default, set):Float;
/**
* An offset for the angle.
* Used for rotated frames.
*/
public var angleOffset(default, set):Float;
/**
* The distance or size of the drop shadow, in pixels,
* relative to the texture itself... NOT the camera.
*/
public var distance(default, set):Float;
/**
* The strength of the drop shadow.
* Effectively just an alpha multiplier.
*/
public var strength(default, set):Float;
/**
* The brightness threshold for the drop shadow.
* Anything below this number will NOT be affected by the drop shadow shader.
* A value of 0 effectively means theres no threshold, and vice versa.
*/
public var threshold(default, set):Float;
/**
* Whether the shader should try and use the alternate mask.
* False by default.
*/
public var useAltMask(default, set):Bool;
/**
* The image for the alternate mask.
* At the moment, it uses the blue channel to specify what is or isn't going to use the alternate threshold.
* TODO: maybe have a sort of "threshold intensity texture" as well? where higher/lower values indicate threshold strength.
*/
public var altMaskImage(default, set):BitmapData;
/**
* An alternate brightness threshold for the drop shadow.
* Anything below this number will NOT be affected by the drop shadow shader,
* but ONLY when the pixel is within the mask.
*/
public var maskThreshold(default, set):Float;
/**
* The FunkinSprite that the shader should get the frame data from.
* Needed to keep the drop shadow shader in the correct bounds and rotation.
*/
public var attachedSprite(default, set):FunkinSprite;
/**
* The hue component of the Adjust Color part of the shader.
*/
public var baseHue(default, set):Float;
/**
* The saturation component of the Adjust Color part of the shader.
*/
public var baseSaturation(default, set):Float;
/**
* The brightness component of the Adjust Color part of the shader.
*/
public var baseBrightness(default, set):Float;
/**
* The contrast component of the Adjust Color part of the shader.
*/
public var baseContrast(default, set):Float;
function makeHueMatrix(h:Float):Array<Float>
{
var c = Math.cos(h);
var s = Math.sin(h);
var wR = 0.299;
var wG = 0.587;
var wB = 0.114;
return [
wR + (1 - wR) * c - wR * s, wG - wG * c - wG * s, wB - wB * c + (1 - wB) * s,
wR - wR * c + 0.143 * s, wG + (1 - wG) * c + 0.140 * s, wB - wB * c - 0.283 * s,
wR - wR * c - (1 - wR) * s, wG - wG * c + wG * s, wB + (1 - wB) * c + wB * s
];
}
function makeSaturationMatrix(s:Float):Array<Float>
{
var lr = 0.2126;
var lg = 0.7152;
var lb = 0.0722;
var inv = 1.0 - s;
return [
lr * inv + s, lg * inv, lb * inv,
lr * inv, lg * inv + s, lb * inv,
lr * inv, lg * inv, lb * inv + s
];
}
function updateAngle():Void
{
var newAngle = (angle + angleOffset) * FlxAngle.TO_RAD;
var cos = Math.cos(newAngle);
var sin = Math.sin(newAngle);
angCos.value = [cos];
angSin.value = [sin];
}
/**
* Sets all 4 adjust color values.
* @param b The brightness value
* @param h The hue value
* @param c The contrast value
* @param s The saturation value
*/
public function setAdjustColor(b:Float, h:Float, c:Float, s:Float):Void
{
baseBrightness = b;
baseHue = h;
baseContrast = c;
baseSaturation = s;
}
function set_baseHue(val:Float):Float
{
baseHue = val;
hueMatrix.value = makeHueMatrix(val * FlxAngle.TO_RAD);
return baseHue;
}
function set_baseSaturation(val:Float):Float
{
baseSaturation = val;
if (val > 0) val *= 3;
val = 1 + (val / 100);
saturationMatrix.value = makeSaturationMatrix(val);
return baseSaturation;
}
function set_baseBrightness(val:Float):Float
{
baseBrightness = val;
brightness.value = [val / 255];
return baseBrightness;
}
function set_baseContrast(val:Float):Float
{
var e:Float = 2.718281828459045;
baseContrast = val;
val = 1 + (val / 100);
if (val > 1.0)
{
val = (((0.00852259 * Math.pow(e, 4.76454 * (val - 1.0))) * 1.01) - 0.0086078159) * 10.0; // Just roll with it...
val += 1.0;
}
contrast.value = [val];
return baseContrast;
}
function set_threshold(val:Float):Float
{
threshold = val;
thr.value = [val];
return val;
}
function set_color(col:FlxColor):FlxColor
{
color = col;
dropColor.value = [color.red / 255, color.green / 255, color.blue / 255];
return color;
}
function set_angle(val:Float):Float
{
angle = val;
updateAngle();
return val;
}
function set_angleOffset(val:Float):Float
{
angleOffset = val;
updateAngle();
return val;
}
function set_distance(val:Float):Float
{
distance = val;
dist.value = [val];
return val;
}
function set_strength(val:Float):Float
{
strength = val;
str.value = [val];
return val;
}
function set_attachedSprite(spr:FunkinSprite):FunkinSprite
{
attachedSprite = spr;
updateFrameInfo(attachedSprite.frame);
// Enable render texture for texture atlas sprites
// This allows the shader to work properly on them
if (attachedSprite.isAnimate && !attachedSprite.useRenderTexture)
{
attachedSprite.useRenderTexture = true;
}
return spr;
}
/**
* Loads an image for the mask.
* While you *could* directly set the value of the mask, this function works for both HTML5 and native targets.
*
* @param path The path to the image to load
*/
public function loadAltMask(path:String):Void
{
altMaskImage = Assets.getBitmapData(path, false);
}
/**
* Should be called on the animation.callback of the attached sprite.
* TODO: figure out why the reference to the attachedSprite breaks on web??
*
* @param name The name of the animation
* @param frameNum The current frame number
* @param frameIndex The current frame index
*/
public function onAttachedFrame(name:String, frameNum:Int, frameIndex:Int):Void
{
if (attachedSprite != null) updateFrameInfo(attachedSprite.frame);
}
/**
* Updates the frame bounds and angle offset of the sprite for the shader
* @param frame The frame to retrieve the information from
*/
public function updateFrameInfo(frame:FlxFrame):Void
{
// NOTE: uv.right is actually the right pos and uv.bottom is the bottom pos
uFrameBounds.value = [frame.uv.left, frame.uv.top, frame.uv.right, frame.uv.bottom];
// if a frame is rotated the shader will look completely wrong lol
angleOffset = frame.angle;
}
function set_altMaskImage(_bitmapData:BitmapData):BitmapData
{
altMask.input = _bitmapData;
return _bitmapData;
}
function set_maskThreshold(val:Float):Float
{
maskThreshold = val;
thr2.value = [val];
return val;
}
function set_useAltMask(val:Bool):Bool
{
useAltMask = val;
useMask.value = [val];
return val;
}
@:glFragmentSource('
#pragma header
#ifdef GL_ES
#ifdef GL_OES_standard_derivatives
#define HAS_DERIVATIVES
#endif
#else
#if __VERSION__ >= 130
#define HAS_DERIVATIVES
#endif
#endif
// This shader aims to mostly recreate how Adobe Animate/Flash handles drop shadows, but its main use here is for rim lighting.
// this shader also includes a recreation of the Animate/Flash "Adjust Color" filter,
// which was kindly provided and written by Rozebud https://github.com/ThatRozebudDude ( thank u rozebud :) )
// Adapted from Andrey-Postelzhuks shader found here: https://forum.unity.com/threads/hue-saturation-brightness-contrast-shader.260649/
// Hue rotation stuff is from here: https://www.w3.org/TR/filter-effects/#feColorMatrixElement
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
uniform float dist;
uniform float str;
uniform float thr;
uniform float angCos;
uniform float angSin;
uniform sampler2D altMask;
uniform bool useMask;
uniform float thr2;
uniform vec3 dropColor;
uniform mat3 hueMatrix;
uniform float contrast;
uniform mat3 saturationMatrix;
uniform float brightness;
const vec3 lumaValue = vec3(0.2126, 0.7152, 0.0722);
vec3 applyHSBCEffect(vec3 color)
{
vec3 bh = (brightness + color) * hueMatrix;
vec3 c = (bh - 0.25) * contrast + 0.25;
vec3 s = c * saturationMatrix;
return s;
}
float getLumaRGB(vec3 color)
{
return dot(color.rgb, lumaValue);
}
vec4 getTexRGBA(vec2 uv)
{
return texture2D(bitmap, uv);
}
float getLumaTex(vec2 uv)
{
return getLumaRGB(getTexRGBA(uv).rgb);
}
// Used as Difference Based AA.
// Return value range (0.0, 2.0)
float lwidth_manual(float center, vec2 uv, vec2 px)
{
vec3 p2x1 = getTexRGBA(uv + vec2( 1.0, 0.0) * px).rgb;
vec3 p1x2 = getTexRGBA(uv + vec2( 0.0, 1.0) * px).rgb;
vec3 p2x2 = getTexRGBA(uv + vec2( 1.0, 1.0) * px).rgb;
float right = getLumaRGB(p2x1);
float down = getLumaRGB(p1x2);
float diagonal = getLumaRGB(p2x2);
float dx = abs(right - center);
float dy = abs(down - center);
float dd = abs(diagonal - center);
return ((dx + dy + dd * 0.7) / (1.0 + 1.0 + 0.7)) * 2.0;
}
// ============================
// THRESHOLD
// ============================
float getThreshold(vec2 uv)
{
float threshold = thr;
float maskIntensity = 0.0;
if (useMask)
{
maskIntensity = texture2D(altMask, uv).b;
if (maskIntensity > 0.0)
threshold = thr2;
}
return threshold;
}
// ============================
// DROP SHADOW / RIM
// ============================
vec4 createDropShadowEx(vec2 uv, vec2 ratio, vec2 size)
{
vec4 color4 = texture2D(bitmap, uv);
#ifdef HAS_DERIVATIVES
// Increase the pixel distance if the screen is smaller than the sprite!
vec2 px = max(ratio, fwidth(uv));
#else
vec2 px = ratio;
#endif
float color3_light = getLumaRGB(color4.rgb);
float delta = lwidth_manual(color3_light, uv, px);
// FIXME: Threshold now uses Luma instead of Gray.
// The -0.05 offset is a temporary hack and does NOT accurately match
// the old behavior, since the difference varies per color.
// Proper fix: adjust thresholds in scripts/mods to account for Luma.
float threshold = getThreshold(uv) - 0.05;
float intensity = smoothstep(threshold - delta, threshold + delta, color3_light);
float shadowAlpha = 0.0;
vec3 color3_no_effect = color4.a > 0.0 ? color4.rgb / color4.a : color4.rgb;
vec3 color3 = applyHSBCEffect(color3_no_effect);
vec2 checked = vec2(
uv.x + (dist * angCos * ratio.x),
uv.y - (dist * angSin * ratio.y)
);
if (checked.x > uFrameBounds.x &&
checked.y > uFrameBounds.y &&
checked.x < uFrameBounds.z &&
checked.y < uFrameBounds.w)
{
shadowAlpha = texture2D(bitmap, checked).a;
}
float rim = (1.0 - (shadowAlpha * str)) * intensity;
color3 += dropColor * rim;
return vec4(color3 * color4.a, color4.a);
}
void main()
{
gl_FragColor = createDropShadowEx(openfl_TextureCoordv, 1.0 / openfl_TextureSize.xy, openfl_TextureSize.xy);
}
')
public function new()
{
super();
angle = 0;
angleOffset = 0;
strength = 1;
distance = 15;
threshold = 0.1;
baseHue = 0;
baseSaturation = 0;
baseBrightness = 0;
baseContrast = 0;
useAltMask = false;
}
}