diff --git a/source/funkin/backend/shaders/BloomEffect.hx b/source/funkin/backend/shaders/BloomEffect.hx new file mode 100644 index 0000000000..da29ccf68b --- /dev/null +++ b/source/funkin/backend/shaders/BloomEffect.hx @@ -0,0 +1,960 @@ +package funkin.backend.shaders; + +import haxe.Timer; +import openfl.filters.BitmapFilter; +import openfl.filters.BitmapFilterShader; +import openfl.display.BitmapData; +import openfl.display.DisplayObjectRenderer; +import openfl.display.BlendMode; +import openfl.display.Shader; +import openfl.geom.Point; +import openfl.geom.Rectangle; + +/** + The BloomEffect class applies a bloom/glow visual effect to display objects. + A bloom effect extracts bright areas from an image, blurs them, and combines + them back to create a glowing halo around bright objects. This effect is + commonly used to simulate intense light, emissive materials, or to add a + dreamy, atmospheric quality to scenes. + + The effect consists of three stages: + 1. Extraction - Bright pixels above a threshold are extracted + 2. Blurring - The extracted bright areas are blurred horizontally and vertically + 3. Combination - The blurred result is blended back with the original image + + You can apply the filter to any display object (objects that inherit from + DisplayObject), such as MovieClip, SimpleButton, TextField, and Video objects, + as well as to BitmapData objects. + + To create a new filter, use the constructor `new BloomEffect()`. The usage + depends on the target object: + + * For display objects: Use the `filters` property (inherited from DisplayObject). + Setting `filters` doesn't modify the object, and filters can be removed by + clearing the `filters` property. + * For BitmapData objects: Use the `BitmapData.applyFilter()` method, which + takes the source BitmapData and filter object, generating a filtered result. + + Applying a filter to a display object sets its `cacheAsBitmap` property to + `true`. Removing all filters restores the original `cacheAsBitmap` value. + + This filter supports Stage scaling but not general scaling, rotation, or skewing. + If the object itself is scaled (`scaleX` and `scaleY` ≠ 100%), the filter + effect doesn't scale—it only scales when the Stage is zoomed. + + A filter won't be applied if the resulting image exceeds maximum dimensions: + * AIR 1.5/Flash Player 10+: 8,191px width/height, 16,777,215 total pixels + * Flash Player 9/AIR 1.1-: 2,880px width/height + + For example, zooming into a large filtered movie clip may disable the filter + if the resulting image exceeds these limits. +**/ +@:access(openfl.geom.Point) +@:access(openfl.geom.Rectangle) +class BloomEffect extends BitmapFilter +{ + /** + [Warning] + You can redefine these shaders, but it is not recommended to modify them without + understanding how they work. You can use CustomShader/FunkinShader to redefine them. + Use setShader() to replace a shader with a custom one. + **/ + @:noCompletion private static var __shaderCache:Map = new Map(); + // I have no idea why, but not using static variable caching causes memory to skyrocket. + + @:noCompletion private var __blurShader:BlurShader; + @:noCompletion private var __combineShader:CombineShader; + @:noCompletion private var __extractShader:ExtractShader; + @:noCompletion private var __extractLowShader:ExtractLowShader; + + public var blurShader(get, never):BlurShader; + public var combineShader(get, never):CombineShader; + public var extractShader(get, never):ExtractShader; + public var extractLowShader(get, never):ExtractLowShader; + + /** + Values that are a power of 2 (such as 2, 4, 8, 16 and 32) are optimized to render + more quickly than other values. + **/ + public var blurX(get, set):Float; + + /** + Values that are a power of 2 (such as 2, 4, 8, 16 and 32) are optimized to render + more quickly than other values. + **/ + public var blurY(get, set):Float; + + /** + The downscaling factor for bloom rendering. Higher values significantly reduce + GPU performance cost, but setting values too high may cause noticeable flickering. + Recommended range is 8-24. + **/ + public var quality(get, set):Float; + + /** + The intensity of the bloom effect. Higher values produce more pronounced bloom. + **/ + public var strength(get, set):Float; + + /** + The brightness threshold for bloom extraction. Pixels brighter than this value + will contribute to the bloom effect. Value range is 0.0 to 1.0. + **/ + public var threshold(get, set):Float; + + /** + Enables extended rendering area to avoid edge artifacts. Enabling this option + will increase performance cost. Generally not required when rendering to camera. + **/ + public var extension(get, set):Bool; + + /** + Enables low-quality extraction to reduce performance cost. When disabled, + reduces flickering but has higher performance impact on non-desktop platforms. + **/ + public var useLowQualityExtract(get, set):Bool; + + /** + The weights for calculating brightness (RGB to grayscale). + Order: [Red, Green, Blue]. Default is [0.2126, 0.7152, 0.0722]. + **/ + public var weights(get, set):Array; + + /** + The blend mode used when combining the bloom with the original image. + Use BlendMode constants (e.g., BlendMode.ADD, BlendMode.SCREEN). + Default is BlendMode.ADD. + **/ + public var blendMode(get, set):BlendMode; + + @:noCompletion private var __blurX:Float; + @:noCompletion private var __blurY:Float; + @:noCompletion private var __horizontalPasses:Int; + @:noCompletion private var __quality:Float; + @:noCompletion private var __verticalPasses:Int; + @:noCompletion private var __strength:Float; + @:noCompletion private var __threshold:Float; + @:noCompletion private var __extension:Bool; + @:noCompletion private var __useLowQualityExtract:Bool; + @:noCompletion private var __weights:Array; + @:noCompletion private var __blendMode:BlendMode; + + #if openfljs + @:noCompletion private static function __init__() + { + untyped Object.defineProperties(BloomEffect.prototype, { + "blurX": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_blurX (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_blurX (v); }") + }, + "blurY": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_blurY (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_blurY (v); }") + }, + "quality": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_quality (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_quality (v); }") + }, + "strength": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_strength (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_strength (v); }") + }, + "threshold": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_threshold (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_threshold (v); }") + }, + "useLowQualityExtract": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_useLowQualityExtract (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_useLowQualityExtract (v); }") + }, + "weights": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_weights (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_weights (v); }") + }, + "blendMode": { + get: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function () { return this.get_blendMode (); }"), + set: untyped #if haxe4 js.Syntax.code #else __js__ #end ("function (v) { return this.set_blendMode (v); }") + }, + }); + } + #end + + /** + Initializes the bloom filter with the specified parameters. + + @param blurX The amount to blur horizontally. + @param blurY The amount to blur vertically. + @param quality The downscaling factor for bloom rendering (higher values reduce + GPU cost but may cause flickering if too high). + @param strength The intensity of the bloom effect. + @param threshold The brightness threshold for bloom extraction (0.0 to 1.0). + @param useLowQualityExtract Enables performance-optimized extraction with + potentially more flickering. + **/ + public function new(blurX:Float = 10, blurY:Float = 10, quality:Float = 8, strength:Float = 1, threshold:Float = 0.6, useLowQualityExtract:Bool = true) + { + super(); + + if (!__shaderCache.exists("blur")) __shaderCache.set("blur", new BlurShader()); + if (!__shaderCache.exists("combine")) __shaderCache.set("combine", new CombineShader()); + if (!__shaderCache.exists("extract")) __shaderCache.set("extract", new ExtractShader()); + if (!__shaderCache.exists("extractLow")) __shaderCache.set("extractLow", new ExtractLowShader()); + + __blurShader = __shaderCache.get("blur"); + __combineShader = __shaderCache.get("combine"); + __extractShader = __shaderCache.get("extract"); + __extractLowShader = __shaderCache.get("extractLow"); + + this.blurX = blurX; + this.blurY = blurY; + this.quality = quality; + this.strength = strength; + this.threshold = threshold; + this.extension = false; + this.useLowQualityExtract = useLowQualityExtract; + this.weights = [0.2126, 0.7152, 0.0722]; + this.blendMode = BlendMode.ADD; + + __needSecondBitmapData = true; + __preserveObject = true; + __renderDirty = true; + } + + public override function clone():BitmapFilter + { + var cloned = new BloomEffect(__blurX, __blurY, __quality, __strength, __threshold, __useLowQualityExtract); + cloned.weights = __weights != null ? __weights.copy() : [0.2126, 0.7152, 0.0722]; + cloned.blendMode = __blendMode; + return cloned; + } + + @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData + { + trace('BloomEffect does not support bitmapData rendering functionality.'); + return sourceBitmapData; + } + + @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader + { + #if !macro + final numBlurPasses = __horizontalPasses + __verticalPasses; + + switch pass + { + case 0: + if (__useLowQualityExtract) + { + __extractLowShader.uThreshold.value = [__threshold]; + __extractLowShader.uQuality.value = [__quality]; + __extractLowShader.uWeights.value = __weights; + return __extractLowShader; + } + else + { + __extractShader.uThreshold.value = [__threshold]; + __extractShader.uQuality.value = [__quality]; + __extractShader.uWeights.value = __weights; + return __extractShader; + } + + case _ if (pass <= numBlurPasses): + final blurPass = pass - 1; + final isHorizontal = blurPass < __horizontalPasses; + + final scalePass = isHorizontal ? blurPass : blurPass - __horizontalPasses; + + final scale = Math.pow(0.5, scalePass >> 1); + final blurRadius = isHorizontal ? blurX * scale : blurY * scale; + + __blurShader.uRadius.value = isHorizontal ? [blurRadius / __quality, 0.0] : [0.0, blurRadius / __quality]; + __blurShader.uQuality.value = [__quality]; + + return __blurShader; + + default: + __combineShader.sourceBitmap.input = sourceBitmapData; + __combineShader.offset.value = [0.0, 0.0]; + __combineShader.uStrength.value = [__strength]; + __combineShader.uThreshold.value = [__threshold]; + __combineShader.uQuality.value = [__quality]; + __combineShader.uBlendMode.value = [cast __blendMode]; + return __combineShader; + } + #else + return null; + #end + } + + // Get & Set Methods + @:noCompletion private function get_blurX():Float + { + return __blurX; + } + + @:noCompletion private function set_blurX(value:Float):Float + { + if (value != __blurX) + { + __blurX = value; + __renderDirty = true; + + if (!__extension) + { + // Setting it to 1 prevents bloom flickering at the screen edges + __leftExtension = 1; + __rightExtension = 1; + } + else + { + __leftExtension = (value > 0 ? Math.ceil(value) : 0); + __rightExtension = __leftExtension; + } + + __horizontalPasses = (value <= 0) ? 0 : Math.ceil(value * 0.0625 / quality) + 1; + __numShaderPasses = __horizontalPasses + __verticalPasses + 2; + } + return value; + } + + @:noCompletion private function get_blurY():Float + { + return __blurY; + } + + @:noCompletion private function set_blurY(value:Float):Float + { + if (value != __blurY) + { + __blurY = value; + __renderDirty = true; + + if (!__extension) + { + // Setting it to 1 prevents bloom flickering at the screen edges + __topExtension = 1; + __bottomExtension = 1; + } + else + { + __topExtension = (value > 0 ? Math.ceil(value) : 0); + __bottomExtension = __topExtension; + } + + __verticalPasses = (value <= 0) ? 0 : Math.ceil(value * 0.0625 / quality) + 1; + __numShaderPasses = __horizontalPasses + __verticalPasses + 2; + } + return value; + } + + @:noCompletion private function get_quality():Float + { + return __quality; + } + + @:noCompletion private function set_quality(value:Float):Float + { + __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * 0.125 / value) + 1; + __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * 0.125 / value) + 1; + __numShaderPasses = __horizontalPasses + __verticalPasses + 2; + + if (value != __quality) + __renderDirty = true; + return __quality = value; + } + + @:noCompletion private function get_strength():Float + { + return __strength; + } + + @:noCompletion private function set_strength(value:Float):Float + { + if (value != __strength) + { + __strength = value; + __renderDirty = true; + } + return value; + } + + @:noCompletion private function get_threshold():Float + { + return __threshold; + } + + @:noCompletion private function set_threshold(value:Float):Float + { + if (value != __threshold) + { + __threshold = value; + __renderDirty = true; + } + return value; + } + + @:noCompletion private function get_extension():Bool + { + return __extension; + } + + @:noCompletion private function set_extension(value:Bool):Bool + { + if (value != __extension) + { + __extension = value; + + if (!value) + { + // Setting it to 1 prevents bloom flickering at the screen edges + __leftExtension = 1; + __rightExtension = 1; + __topExtension = 1; + __bottomExtension = 1; + } + else + { + __leftExtension = (__blurX > 0 ? Math.ceil(__blurX) : 0); + __rightExtension = __leftExtension; + __topExtension = (__blurY > 0 ? Math.ceil(__blurY) : 0); + __bottomExtension = __topExtension; + } + + __renderDirty = true; + } + return value; + } + + @:noCompletion private function get_useLowQualityExtract():Bool + { + return __useLowQualityExtract; + } + + @:noCompletion private function set_useLowQualityExtract(value:Bool):Bool + { + if (value != __useLowQualityExtract) + { + __useLowQualityExtract = value; + __renderDirty = true; + } + return value; + } + + @:noCompletion private function get_weights():Array + { + return __weights; + } + + @:noCompletion private function set_weights(value:Array):Array + { + if (value != __weights) + { + __weights = value; + __renderDirty = true; + } + return value; + } + + @:noCompletion private function get_blendMode():BlendMode + { + return __blendMode; + } + + @:noCompletion private function set_blendMode(value:BlendMode):BlendMode + { + if (value != __blendMode) + { + __blendMode = value; + __renderDirty = true; + } + return value; + } + + @:noCompletion private function get_blurShader():BlurShader + { + return __blurShader; + } + + @:noCompletion private function get_combineShader():CombineShader + { + return __combineShader; + } + + @:noCompletion private function get_extractShader():ExtractShader + { + return __extractShader; + } + + @:noCompletion private function get_extractLowShader():ExtractLowShader + { + return __extractLowShader; + } + + /** + Replaces a shader with a custom one. + + @param type The shader type to replace. Valid values: "blur", "combine", "extract", "extractLow" + @param shader The new shader instance to use + + Usage: + ```haxe + var bloom = new BloomEffect(); + bloom.setShader("combine", new MyCustomCombineShader()); + bloom.setShader("blur", new MyCustomBlurShader()); + ``` + **/ + public function setShader(type:String, shader:Dynamic):Void + { + switch (type) + { + case "blur": + __blurShader = cast shader; + case "combine": + __combineShader = cast shader; + case "extract": + __extractShader = cast shader; + case "extractLow": + __extractLowShader = cast shader; + } + __renderDirty = true; + } +} + +private class BlurShader extends BitmapFilterShader +{ + @:glFragmentSource(" + uniform sampler2D openfl_Texture; + + varying mat2 vBlurCoord0; + varying mat2 vBlurCoord1; + varying vec2 vBlurCoord2; + varying mat2 vBlurCoord3; + varying mat2 vBlurCoord4; + + void main(void) { + if ((all(greaterThanEqual(vBlurCoord2, vec2(0.0))) && all(lessThanEqual(vBlurCoord2, vec2(1.0)))) == false) return; + + vec4 sum = texture2D(openfl_Texture, vBlurCoord0[0]) * 0.028532; + sum += texture2D(openfl_Texture, vBlurCoord0[1]) * 0.067234; + sum += texture2D(openfl_Texture, vBlurCoord1[0]) * 0.124009; + sum += texture2D(openfl_Texture, vBlurCoord1[1]) * 0.179044; + sum += texture2D(openfl_Texture, vBlurCoord2) * 0.202360; + sum += texture2D(openfl_Texture, vBlurCoord3[0]) * 0.179044; + sum += texture2D(openfl_Texture, vBlurCoord3[1]) * 0.124009; + sum += texture2D(openfl_Texture, vBlurCoord4[0]) * 0.067234; + sum += texture2D(openfl_Texture, vBlurCoord4[1]) * 0.028532; + gl_FragColor = sum; + } + ") + @:glVertexSource(" + attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + + uniform mat4 openfl_Matrix; + + uniform vec2 uRadius; + uniform vec2 uTextureSize; + uniform float uQuality; + + varying mat2 vBlurCoord0; + varying mat2 vBlurCoord1; + varying vec2 vBlurCoord2; + varying mat2 vBlurCoord3; + varying mat2 vBlurCoord4; + + void main(void) { + vec4 pos = openfl_Position; + pos.xy /= uQuality; + gl_Position = openfl_Matrix * pos; + + vec2 r = uRadius / uTextureSize; + vec2 coord = openfl_TextureCoord / uQuality; + vBlurCoord0[0] = coord - r; + vBlurCoord0[1] = coord - r * 0.25; + vBlurCoord1[0] = coord - r * 0.5; + vBlurCoord1[1] = coord - r * 0.75; + vBlurCoord2 = coord; + vBlurCoord3[0] = coord + r * 0.25; + vBlurCoord3[1] = coord + r * 0.5; + vBlurCoord4[0] = coord + r * 0.75; + vBlurCoord4[1] = coord + r; + } + ") + public function new() + { + super(); + + #if !macro + uRadius.value = [0, 0]; + #end + } + + @:noCompletion private override function __update():Void + { + #if !macro + uTextureSize.value = [__texture.input.width, __texture.input.height]; + #end + + super.__update(); + } +} + +private class ExtractLowShader extends BitmapFilterShader +{ + @:glFragmentSource(" + uniform sampler2D openfl_Texture; + uniform float uThreshold; + uniform vec3 uWeights; + varying vec2 vTexCoord; + + void main(void) { + if ((all(greaterThanEqual(vTexCoord, vec2(0.0))) && all(lessThanEqual(vTexCoord, vec2(1.0)))) == false) return; + + vec4 texel = texture2D(openfl_Texture, vTexCoord); + float brightness = min(dot(texel.rgb, uWeights), 1.0); + float mask = smoothstep(uThreshold, uThreshold + 0.1, brightness); + gl_FragColor = texel * mask; + } + ") + @:glVertexSource(" + attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 openfl_TextureSize; + uniform float uQuality; + varying vec2 vTexCoord; + + void main(void) { + vec4 pos = openfl_Position; + pos.xy /= uQuality; + gl_Position = openfl_Matrix * pos; + vTexCoord = openfl_TextureCoord; + } + ") + public function new() + { + super(); + + #if !macro + uThreshold.value = [0.6]; + uWeights.value = [0.2126, 0.7152, 0.0722]; + #end + } +} + +private class ExtractShader extends BitmapFilterShader +{ + @:glFragmentSource(" + uniform sampler2D openfl_Texture; + uniform vec2 openfl_TextureSize; + uniform float uThreshold; + uniform float uQuality; + uniform vec3 uWeights; + varying vec2 vTexCoord; + varying vec4 border; + + void main(void) { + if ((all(greaterThanEqual(vTexCoord, border.xy)) && all(lessThanEqual(vTexCoord, border.zw))) == false) return; + + float quality = floor(uQuality) / 2.0; + vec2 texelSize = 1.0 / openfl_TextureSize; + + vec4 accumulated = vec4(0.0); + int sampleCount = 0; + + + for (float dx = -quality; dx <= quality; dx += 2.0) { + for (float dy = -quality; dy <= quality; dy += 2.0) { + vec2 sampleCoord = vTexCoord + vec2(dx, dy) * texelSize; + + vec4 texel = texture2D(openfl_Texture, sampleCoord); + float brightness = min(dot(texel.rgb, uWeights), 1.0); + float mask = smoothstep(uThreshold, uThreshold + 0.1, brightness); + accumulated += texel * mask; + sampleCount++; + } + } + + gl_FragColor = accumulated / float(sampleCount); + } + ") + @:glVertexSource(" + attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 openfl_TextureSize; + uniform float uQuality; + varying vec2 vTexCoord; + varying vec4 border; + + void main(void) { + vec4 pos = openfl_Position; + pos.xy /= uQuality; + + vec2 size = 1.0 / openfl_TextureSize * uQuality; + border = vec4(size, vec2(1.0) - size); + + gl_Position = openfl_Matrix * pos; + vTexCoord = openfl_TextureCoord; + } + ") + public function new() + { + super(); + + #if !macro + uThreshold.value = [0.6]; + uWeights.value = [0.2126, 0.7152, 0.0722]; + #end + } +} + +private class CombineShader extends BitmapFilterShader +{ + @:glFragmentSource(" + uniform sampler2D openfl_Texture; + uniform sampler2D sourceBitmap; + uniform float uStrength; + uniform float uThreshold; + uniform int uBlendMode; + varying vec4 textureCoords; + + vec4 blendAdd(vec4 src, vec4 bloom) { + return src + bloom; + } + + vec4 blendScreen(vec4 src, vec4 bloom) { + return vec4(1.0) - (vec4(1.0) - src) * (vec4(1.0) - bloom); + } + + vec4 blendMultiply(vec4 src, vec4 bloom) { + return src * bloom; + } + + vec4 blendLighten(vec4 src, vec4 bloom) { + return max(src, bloom); + } + + vec4 blendDarken(vec4 src, vec4 bloom) { + return min(src, bloom); + } + + vec4 blendOverlay(vec4 src, vec4 bloom) { + vec4 result = vec4(0.0); + for(int i = 0; i < 4; i++) { + if(src[i] < 0.5) { + result[i] = 2.0 * src[i] * bloom[i]; + } else { + result[i] = 1.0 - 2.0 * (1.0 - src[i]) * (1.0 - bloom[i]); + } + } + return result; + } + + vec4 blendColorDodge(vec4 src, vec4 bloom) { + vec4 result = vec4(0.0); + for(int i = 0; i < 4; i++) { + if(bloom[i] < 1.0) { + result[i] = min(1.0, src[i] / (1.0 - bloom[i])); + } else { + result[i] = 1.0; + } + } + return result; + } + + vec4 blendColorBurn(vec4 src, vec4 bloom) { + vec4 result = vec4(0.0); + for(int i = 0; i < 4; i++) { + if(bloom[i] > 0.0) { + result[i] = max(0.0, 1.0 - (1.0 - src[i]) / bloom[i]); + } else { + result[i] = 0.0; + } + } + return result; + } + + vec4 blendSoftLight(vec4 src, vec4 bloom) { + vec4 result = vec4(0.0); + for(int i = 0; i < 4; i++) { + if(bloom[i] < 0.5) { + result[i] = src[i] - (1.0 - 2.0 * bloom[i]) * src[i] * (1.0 - src[i]); + } else { + float d = (src[i] <= 0.25) ? ((16.0 * src[i] - 12.0) * src[i] + 4.0) * src[i] : sqrt(src[i]); + result[i] = src[i] + (2.0 * bloom[i] - 1.0) * (d - src[i]); + } + } + return result; + } + + vec4 blendHardLight(vec4 src, vec4 bloom) { + return blendOverlay(bloom, src); + } + + vec4 blendDifference(vec4 src, vec4 bloom) { + return abs(src - bloom); + } + + vec4 blendExclusion(vec4 src, vec4 bloom) { + return src + bloom - 2.0 * src * bloom; + } + + vec4 blendAlpha(vec4 src, vec4 bloom) { + return src + bloom * (1.0 - src.a); + } + + vec3 rgb2hsl(vec3 color) { + float maxC = max(max(color.r, color.g), color.b); + float minC = min(min(color.r, color.g), color.b); + float l = (maxC + minC) / 2.0; + float h = 0.0; + float s = 0.0; + if(maxC != minC) { + float d = maxC - minC; + s = l > 0.5 ? d / (2.0 - maxC - minC) : d / (maxC + minC); + if(maxC == color.r) { + h = (color.g - color.b) / d + (color.g < color.b ? 6.0 : 0.0); + } else if(maxC == color.g) { + h = (color.b - color.r) / d + 2.0; + } else { + h = (color.r - color.g) / d + 4.0; + } + h /= 6.0; + } + return vec3(h, s, l); + } + + float hue2rgb(float p, float q, float t) { + if(t < 0.0) t += 1.0; + if(t > 1.0) t -= 1.0; + if(t < 1.0/6.0) return p + (q - p) * 6.0 * t; + if(t < 1.0/2.0) return q; + if(t < 2.0/3.0) return p + (q - p) * (2.0/3.0 - t) * 6.0; + return p; + } + + vec3 hsl2rgb(vec3 hsl) { + float r, g, b; + if(hsl.y == 0.0) { + r = g = b = hsl.z; + } else { + float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y; + float p = 2.0 * hsl.z - q; + r = hue2rgb(p, q, hsl.x + 1.0/3.0); + g = hue2rgb(p, q, hsl.x); + b = hue2rgb(p, q, hsl.x - 1.0/3.0); + } + return vec3(r, g, b); + } + + vec4 blendHue(vec4 src, vec4 bloom) { + vec3 srcHSL = rgb2hsl(src.rgb); + vec3 bloomHSL = rgb2hsl(bloom.rgb); + vec3 resultRGB = hsl2rgb(vec3(bloomHSL.x, srcHSL.y, srcHSL.z)); + return vec4(resultRGB, src.a); + } + + vec4 blendSaturation(vec4 src, vec4 bloom) { + vec3 srcHSL = rgb2hsl(src.rgb); + vec3 bloomHSL = rgb2hsl(bloom.rgb); + vec3 resultRGB = hsl2rgb(vec3(srcHSL.x, bloomHSL.y, srcHSL.z)); + return vec4(resultRGB, src.a); + } + + vec4 blendColor(vec4 src, vec4 bloom) { + vec3 srcHSL = rgb2hsl(src.rgb); + vec3 bloomHSL = rgb2hsl(bloom.rgb); + vec3 resultRGB = hsl2rgb(vec3(bloomHSL.x, bloomHSL.y, srcHSL.z)); + return vec4(resultRGB, src.a); + } + + vec4 blendLuminosity(vec4 src, vec4 bloom) { + vec3 srcHSL = rgb2hsl(src.rgb); + vec3 bloomHSL = rgb2hsl(bloom.rgb); + vec3 resultRGB = hsl2rgb(vec3(srcHSL.x, srcHSL.y, bloomHSL.z)); + return vec4(resultRGB, src.a); + } + + void main(void) { + vec4 src = texture2D(sourceBitmap, textureCoords.xy); + vec4 bloom = texture2D(openfl_Texture, textureCoords.zw) * uStrength; + + vec4 result; + if(uBlendMode == 0) { + result = src + bloom; + } else if(uBlendMode == 1) { + result = blendAlpha(src, bloom); + } else if(uBlendMode == 2) { + result = blendDarken(src, bloom); + } else if(uBlendMode == 3) { + result = blendDifference(src, bloom); + } else if(uBlendMode == 4) { + result = vec4(src.rgb, src.a - bloom.a); + } else if(uBlendMode == 5) { + result = blendHardLight(src, bloom); + } else if(uBlendMode == 6) { + result = vec4(1.0) - src; + } else if(uBlendMode == 7) { + result = src; + } else if(uBlendMode == 8) { + result = blendLighten(src, bloom); + } else if(uBlendMode == 9) { + result = blendMultiply(src, bloom); + } else if(uBlendMode == 10) { + result = bloom; + } else if(uBlendMode == 11) { + result = blendOverlay(src, bloom); + } else if(uBlendMode == 12) { + result = blendScreen(src, bloom); + } else if(uBlendMode == 13) { + result = src + bloom; + } else if(uBlendMode == 14) { + result = src + bloom - 1.0; + } else if(uBlendMode == 15) { + result = blendColorDodge(src, bloom); + } else if(uBlendMode == 16) { + result = blendColorBurn(src, bloom); + } else if(uBlendMode == 17) { + result = blendSoftLight(src, bloom); + } else if(uBlendMode == 18) { + result = blendExclusion(src, bloom); + } else if(uBlendMode == 19) { + result = blendHue(src, bloom); + } else if(uBlendMode == 20) { + result = blendSaturation(src, bloom); + } else if(uBlendMode == 21) { + result = blendColor(src, bloom); + } else if(uBlendMode == 22) { + result = blendLuminosity(src, bloom); + } else { + result = src + bloom; + } + + gl_FragColor = result; + } + ") + @:glVertexSource("attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 openfl_TextureSize; + uniform vec2 offset; + uniform float uQuality; + varying vec4 textureCoords; + + void main(void) { + gl_Position = openfl_Matrix * openfl_Position; + textureCoords = vec4(openfl_TextureCoord, (openfl_TextureCoord - offset / openfl_TextureSize) / uQuality); + } + ") + public function new() + { + super(); + + #if !macro + offset.value = [0, 0]; + uStrength.value = [1.0]; + uThreshold.value = [0.6]; + uBlendMode.value = [0]; + #end + } +} \ No newline at end of file