From 672d415c306293ea35ce9775ae6c106e6ff53e81 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Mon, 31 Aug 2026 20:23:47 +0200 Subject: [PATCH 01/21] BlurFilter: Flash-faithful box blur behind -Dflash_box_blur OpenFL's blur is a fixed 7-tap Gaussian stacked round(blur*quality/4)+1 times at halving radii -- a heuristic that doesn't match Flash: too soft at low quality, too tight/undersampled at large radii (visible faceting). Add a fractional separable box blur (Ruffle's algorithm, faithful to Flash Player) behind -Dflash_box_blur, keeping the Gaussian as the default so the two can be A/B'd. Per axis, per pass: full_size = blur (<=255); radius = (full_size-1)/2; m = ceil(radius)-1 interior double-weighted bilinear pairs; alpha = fractional edge weight quantised to 8 bits (imitating Flash's fixed point); first/last edge taps weighted alpha and alpha+1; normalise by full_size; 8-bit round each pass. Passes = quality*2 (one H + one V per quality iteration; separable box passes commute). Shader loop is constant-bounded (64 pairs) with a dynamic break for WebGL1 compatibility. Verified on the filter grid vs the Flash/AIR reference: box blur matches Flash's crispness at quality 1 and its directional/wide spread at quality 3 and asymmetric radii, where the Gaussian did not. Glow/shadow/ bevel are unaffected (they use their own internal blur). Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BlurFilter.hx | 125 +++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index f7bab3f223..20b65af95a 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -70,6 +70,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:final class BlurFilter extends BitmapFilter { @:noCompletion private static var __blurShader:BlurShader = new BlurShader(); + #if flash_box_blur + // Flash-faithful fractional box blur (Ruffle-style), gated behind + // -Dflash_box_blur. Default build keeps the 7-tap Gaussian above. + @:noCompletion private static var __boxBlurShader:BoxBlurShader = new BoxBlurShader(); + #end /** The amount of horizontal blur. Valid values are from 0 to 255(floating @@ -194,6 +199,41 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader { + #if flash_box_blur + #if !macro + // Passes alternate horizontal / vertical; each applies one full box blur + // for its axis, iterated `quality` times (separable box passes commute). + var horizontal = (pass % 2 == 0); + var v = horizontal ? blurX : blurY; + var fullSize = v > 255 ? 255.0 : v; + __boxBlurShader.uDir.value = horizontal ? [1.0, 0.0] : [0.0, 1.0]; + if (fullSize <= 1) + { + // noop pass: sample the centre pixel unchanged + __boxBlurShader.uFullSize.value = [1.0]; + __boxBlurShader.uM.value = [0.0]; + __boxBlurShader.uM2.value = [0.0]; + __boxBlurShader.uFirstWeight.value = [0.0]; + __boxBlurShader.uLastOffset.value = [0.0]; + __boxBlurShader.uLastWeight.value = [1.0]; + } + else + { + var radius = (fullSize - 1) / 2; + var m = Math.ceil(radius) - 1; + if (m < 0) m = 0; + // fractional edge weight, 8-bit quantised to imitate Flash's fixed point + var alpha = Math.floor((radius - m) * 255) / 255; + __boxBlurShader.uFullSize.value = [fullSize]; + __boxBlurShader.uM.value = [m]; + __boxBlurShader.uM2.value = [m * 2]; + __boxBlurShader.uFirstWeight.value = [alpha]; + __boxBlurShader.uLastOffset.value = [alpha / (alpha + 1)]; + __boxBlurShader.uLastWeight.value = [alpha + 1]; + } + #end + return __boxBlurShader; + #else #if !macro if (pass < __horizontalPasses) { @@ -210,6 +250,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO #end return __blurShader; + #end } @:noCompletion inline function __padFor(value:Float):Int @@ -272,10 +313,18 @@ import lime._internal.graphics.ImageDataUtil; // TODO { // TODO: Quality effect with fewer passes? + #if flash_box_blur + // one horizontal + one vertical box pass per quality iteration + var passes = (value > 0) ? value : 1; + __horizontalPasses = passes; + __verticalPasses = passes; + __numShaderPasses = passes * 2; + #else __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (value / 4)) + 1; __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (value / 4)) + 1; __numShaderPasses = __horizontalPasses + __verticalPasses; + #end if (value != __quality) __renderDirty = true; __quality = value; @@ -350,6 +399,82 @@ private class BlurShader extends BitmapFilterShader super.__update(); } } + +#if flash_box_blur +// Flash-faithful fractional box blur, one axis per pass (Ruffle-style). Kernel: +// full_size = blur (<=255); radius = (full_size-1)/2; m = ceil(radius)-1 interior +// double-weighted bilinear pairs; alpha = frac edge weight (8-bit quantised); +// normalise by full_size; 8-bit round each pass to imitate Flash's fixed point. +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +private class BoxBlurShader extends BitmapFilterShader +{ + @:glVertexSource("#pragma header + + void main(void) { + + #pragma body + + }") + @:glFragmentSource("#pragma header + + uniform vec2 uTextureSize; + uniform vec2 uDir; + uniform float uFullSize; + uniform float uM; + uniform float uM2; + uniform float uFirstWeight; + uniform float uLastOffset; + uniform float uLastWeight; + + void main(void) { + + vec2 direction = uDir / uTextureSize; + vec2 base = openfl_TextureCoordv - direction * uM; + + vec4 total = texture2D(openfl_Texture, base - direction) * uFirstWeight; + + vec4 center = vec4(0.0); + for (int i = 0; i < 64; i++) { + float fi = float(i) * 2.0 + 0.5; + if (fi >= uM2) break; + center += texture2D(openfl_Texture, base + direction * fi); + } + total += center * 2.0; + + total += texture2D(openfl_Texture, base + direction * (uM2 + uLastOffset)) * uLastWeight; + + vec4 result = total / uFullSize; + gl_FragColor = floor(result * 255.0) / 255.0; + + }") + public function new() + { + super(); + + #if !macro + uDir.value = [1.0, 0.0]; + uFullSize.value = [1.0]; + uM.value = [0.0]; + uM2.value = [0.0]; + uFirstWeight.value = [0.0]; + uLastOffset.value = [0.0]; + uLastWeight.value = [1.0]; + #end + } + + @:noCompletion private override function __update():Void + { + #if !macro + uTextureSize.value = [__texture.input.width, __texture.input.height]; + #end + + super.__update(); + } +} +#end #else typedef BlurFilter = flash.filters.BlurFilter; #end From c92ccd8842dae004a8f64024008e3cb41a52b9c5 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Mon, 31 Aug 2026 21:12:02 +0200 Subject: [PATCH 02/21] filters: box blur for glow/shadow/bevel too (-Dflash_box_blur) The box blur added to BlurFilter only fixed the standalone blur. Glow, drop shadow and bevel each have their OWN copy of the old too-tight Gaussian heuristic (round(blur*quality/4)+1 passes of a 6/7-tap Gaussian at halving radii), so their blur did not match Flash: shadows read weaker/tighter than the reference and bevels were soft/diffuse (looked like a resolution loss), in both single and stacked use, on every target. Extend the box blur to all three, behind the same -Dflash_box_blur gate: - BlurFilter: extract __setupBoxBlur(horizontal, v) static helper. - GlowFilter: BoxBlurAlphaShader (fractional box on the alpha channel, same colourise+strength as BlurAlphaShader) + __setupBoxBlur helper; passes = quality*2. Shared by DropShadowFilter (glow-with-offset). - BevelFilter: reuse BlurFilter.__setupBoxBlur; passes = quality*2. Default (Gaussian) path unchanged. Verified on the grid vs Flash/AIR: bevels are now crisp (matching Flash), and drop/inner shadows spread correctly, single and stacked. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BevelFilter.hx | 10 ++ src/openfl/filters/BlurFilter.hx | 71 +++++++------ src/openfl/filters/DropShadowFilter.hx | 14 ++- src/openfl/filters/GlowFilter.hx | 133 ++++++++++++++++++++++++- 4 files changed, 196 insertions(+), 32 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 50482c7b4f..3e963520d7 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -173,6 +173,10 @@ import lime._internal.graphics.ImageDataUtil; var numBlurPasses = __horizontalPasses + __verticalPasses; if (blurPass < numBlurPasses) { + #if flash_box_blur + var horizontal = pass < __horizontalPasses; + return BlurFilter.__setupBoxBlur(horizontal, horizontal ? blurX : blurY); + #else var shader = BlurFilter.__blurShader; if (pass < __horizontalPasses) { @@ -187,6 +191,7 @@ import lime._internal.graphics.ImageDataUtil; shader.uRadius.value[1] = blurY * scale; } return shader; + #end } __bevelShader.sourceBitmap.input = sourceBitmapData; @@ -346,8 +351,13 @@ import lime._internal.graphics.ImageDataUtil; value = value < 1 ? 1 : value; value = value > 15 ? 15 : value; + #if flash_box_blur + __horizontalPasses = (__blurX <= 0) ? 0 : value; + __verticalPasses = (__blurY <= 0) ? 0 : value; + #else __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (value / 4)); __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (value / 4)); + #end __numShaderPasses = __horizontalPasses + __verticalPasses + 1; diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index 20b65af95a..feabf0bbe3 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -201,38 +201,13 @@ import lime._internal.graphics.ImageDataUtil; // TODO { #if flash_box_blur #if !macro - // Passes alternate horizontal / vertical; each applies one full box blur - // for its axis, iterated `quality` times (separable box passes commute). + // passes alternate horizontal / vertical; each applies one full box blur + // for its axis, iterated `quality` times (separable box passes commute) var horizontal = (pass % 2 == 0); - var v = horizontal ? blurX : blurY; - var fullSize = v > 255 ? 255.0 : v; - __boxBlurShader.uDir.value = horizontal ? [1.0, 0.0] : [0.0, 1.0]; - if (fullSize <= 1) - { - // noop pass: sample the centre pixel unchanged - __boxBlurShader.uFullSize.value = [1.0]; - __boxBlurShader.uM.value = [0.0]; - __boxBlurShader.uM2.value = [0.0]; - __boxBlurShader.uFirstWeight.value = [0.0]; - __boxBlurShader.uLastOffset.value = [0.0]; - __boxBlurShader.uLastWeight.value = [1.0]; - } - else - { - var radius = (fullSize - 1) / 2; - var m = Math.ceil(radius) - 1; - if (m < 0) m = 0; - // fractional edge weight, 8-bit quantised to imitate Flash's fixed point - var alpha = Math.floor((radius - m) * 255) / 255; - __boxBlurShader.uFullSize.value = [fullSize]; - __boxBlurShader.uM.value = [m]; - __boxBlurShader.uM2.value = [m * 2]; - __boxBlurShader.uFirstWeight.value = [alpha]; - __boxBlurShader.uLastOffset.value = [alpha / (alpha + 1)]; - __boxBlurShader.uLastWeight.value = [alpha + 1]; - } - #end + return __setupBoxBlur(horizontal, horizontal ? blurX : blurY); + #else return __boxBlurShader; + #end #else #if !macro if (pass < __horizontalPasses) @@ -253,6 +228,42 @@ import lime._internal.graphics.ImageDataUtil; // TODO #end } + #if flash_box_blur + // Configure the shared box-blur shader for one axis of one pass. Reused by + // BevelFilter (which blurs the source before deriving highlight/shadow). + @:noCompletion private static function __setupBoxBlur(horizontal:Bool, v:Float):BitmapFilterShader + { + var s = __boxBlurShader; + var fullSize = v > 255 ? 255.0 : v; + s.uDir.value[0] = horizontal ? 1.0 : 0.0; + s.uDir.value[1] = horizontal ? 0.0 : 1.0; + if (fullSize <= 1) + { + s.uFullSize.value[0] = 1.0; + s.uM.value[0] = 0.0; + s.uM2.value[0] = 0.0; + s.uFirstWeight.value[0] = 0.0; + s.uLastOffset.value[0] = 0.0; + s.uLastWeight.value[0] = 1.0; + } + else + { + var radius = (fullSize - 1) / 2; + var m = Math.ceil(radius) - 1; + if (m < 0) m = 0; + // fractional edge weight, 8-bit quantised to imitate Flash's fixed point + var frac = Math.floor((radius - m) * 255) / 255; + s.uFullSize.value[0] = fullSize; + s.uM.value[0] = m; + s.uM2.value[0] = m * 2; + s.uFirstWeight.value[0] = frac; + s.uLastOffset.value[0] = frac / (frac + 1); + s.uLastWeight.value[0] = frac + 1; + } + return s; + } + #end + @:noCompletion inline function __padFor(value:Float):Int { if (value <= 0) return 0; diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index 1a5225b026..b8e3f88d49 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -324,6 +324,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (blurPass < numBlurPasses) { + var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; + #if flash_box_blur + var horizontal = blurPass < __horizontalPasses; + return GlowFilter.__setupBoxBlur(horizontal, horizontal ? blurX : blurY, color, alpha, strength); + #else var shader = GlowFilter.__blurAlphaShader; if (blurPass < __horizontalPasses) { @@ -341,8 +346,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; shader.uColor.value[2] = (color & 0xFF) / 255; shader.uColor.value[3] = alpha; - shader.uStrength.value[0] = blurPass == (numBlurPasses - 1) ? __strength : 1.0; + shader.uStrength.value[0] = strength; return shader; + #end } if (__inner) { @@ -402,8 +408,14 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __calculateNumShaderPasses():Void { + #if flash_box_blur + var q = (__quality > 0) ? __quality : 1; + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; + #else __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; + #end __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index bb9905ee06..cb1298baec 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -69,6 +69,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO { @:noCompletion private static var __invertAlphaShader = new InvertAlphaShader(); @:noCompletion private static var __blurAlphaShader = new BlurAlphaShader(); + #if flash_box_blur + // Flash-faithful fractional box blur of the alpha channel (Ruffle-style), + // colourised like BlurAlphaShader. Shared by GlowFilter + DropShadowFilter. + @:noCompletion private static var __boxBlurAlphaShader = new BoxBlurAlphaShader(); + #end @:noCompletion private static var __combineShader = new CombineShader(); @:noCompletion private static var __innerCombineShader = new InnerCombineShader(); @:noCompletion private static var __combineKnockoutShader = new CombineKnockoutShader(); @@ -286,6 +291,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (blurPass < numBlurPasses) { + var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; + #if flash_box_blur + var horizontal = blurPass < __horizontalPasses; + return __setupBoxBlur(horizontal, horizontal ? blurX : blurY, color, alpha, strength); + #else var shader = __blurAlphaShader; if (blurPass < __horizontalPasses) { @@ -303,8 +313,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; shader.uColor.value[2] = (color & 0xFF) / 255; shader.uColor.value[3] = alpha; - shader.uStrength.value[0] = blurPass == (numBlurPasses - 1) ? __strength : 1.0; + shader.uStrength.value[0] = strength; return shader; + #end } if (__inner) { @@ -354,11 +365,58 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __calculateNumShaderPasses():Void { + #if flash_box_blur + // one horizontal + one vertical box pass per quality iteration + var q = (__quality > 0) ? __quality : 1; + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; + #else __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; + #end __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } + #if flash_box_blur + // Configure the shared box-blur-alpha shader for one axis/pass (used by both + // GlowFilter and DropShadowFilter). Same kernel math as BlurFilter's box blur. + @:noCompletion private static function __setupBoxBlur(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader + { + var s = __boxBlurAlphaShader; + var fullSize = v > 255 ? 255.0 : v; + s.uDir.value[0] = horizontal ? 1.0 : 0.0; + s.uDir.value[1] = horizontal ? 0.0 : 1.0; + if (fullSize <= 1) + { + s.uFullSize.value[0] = 1.0; + s.uM.value[0] = 0.0; + s.uM2.value[0] = 0.0; + s.uFirstWeight.value[0] = 0.0; + s.uLastOffset.value[0] = 0.0; + s.uLastWeight.value[0] = 1.0; + } + else + { + var radius = (fullSize - 1) / 2; + var m = Math.ceil(radius) - 1; + if (m < 0) m = 0; + var frac = Math.floor((radius - m) * 255) / 255; + s.uFullSize.value[0] = fullSize; + s.uM.value[0] = m; + s.uM2.value[0] = m * 2; + s.uFirstWeight.value[0] = frac; + s.uLastOffset.value[0] = frac / (frac + 1); + s.uLastWeight.value[0] = frac + 1; + } + s.uColor.value[0] = ((color >> 16) & 0xFF) / 255; + s.uColor.value[1] = ((color >> 8) & 0xFF) / 255; + s.uColor.value[2] = (color & 0xFF) / 255; + s.uColor.value[3] = alpha; + s.uStrength.value[0] = strength; + return s; + } + #end + // Get & Set Methods @:noCompletion private function get_alpha():Float { @@ -576,6 +634,79 @@ private class BlurAlphaShader extends BitmapFilterShader } } +#if flash_box_blur +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +private class BoxBlurAlphaShader extends BitmapFilterShader +{ + @:glFragmentSource("#pragma header + + uniform vec4 uColor; + uniform float uStrength; + uniform vec2 uTextureSize; + uniform vec2 uDir; + uniform float uFullSize; + uniform float uM; + uniform float uM2; + uniform float uFirstWeight; + uniform float uLastOffset; + uniform float uLastWeight; + + void main(void) + { + vec2 direction = uDir / uTextureSize; + vec2 base = openfl_TextureCoordv - direction * uM; + + float total = texture2D(openfl_Texture, base - direction).a * uFirstWeight; + + float center = 0.0; + for (int i = 0; i < 64; i++) { + float fi = float(i) * 2.0 + 0.5; + if (fi >= uM2) break; + center += texture2D(openfl_Texture, base + direction * fi).a; + } + total += center * 2.0; + + total += texture2D(openfl_Texture, base + direction * (uM2 + uLastOffset)).a * uLastWeight; + + float a = total / uFullSize; + gl_FragColor = uColor * clamp(a * uStrength, 0.0, 1.0); + }") + @:glVertexSource("#pragma header + + void main(void) { + + #pragma body + + }") + public function new() + { + super(); + #if !macro + uColor.value = [0, 0, 0, 0]; + uStrength.value = [1]; + uDir.value = [1, 0]; + uFullSize.value = [1]; + uM.value = [0]; + uM2.value = [0]; + uFirstWeight.value = [0]; + uLastOffset.value = [0]; + uLastWeight.value = [1]; + #end + } + + @:noCompletion private override function __update():Void + { + #if !macro + uTextureSize.value = [__texture.input.width, __texture.input.height]; + #end + super.__update(); + } +} +#end + #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug From 1cbd34bb77c230207b6c369f87ae34d116b94ee6 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Mon, 31 Aug 2026 21:53:53 +0200 Subject: [PATCH 03/21] filters: add GradientGlowFilter (missing from OpenFL) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GradientGlowFilter did not exist in OpenFL (in the Flash IDE menu; the SWF parser flagged it "not supported on native"). No Ruffle reference — even Ruffle's GPU backend leaves it unimplemented — so matched against Flash/AIR empirically. Implementation (GL shader path): blur the object's alpha into a soft distance field (reusing BlurFilter's box blur under -Dflash_box_blur, or the Gaussian otherwise), build a 256-entry ramp from (colors, alphas, ratios) with ratio 0 = outer / 255 = inner, then a combine shader indexes the ramp by the field and composites per type: outer = ramp*(1-src.a) over src; inner = ramp masked by src.a over src; full = ramp over src everywhere; knockout drops src. distance/angle offset the field like a drop shadow. Falls back to the flash.filters typedef on the flash target. Verified vs Flash/AIR on a magenta->yellow->cyan gradient: outer, inner, full, knockout, fade-out alpha, distance offset and strength all match. Software __applyFilter is a no-op placeholder (GL path is what renders on-screen). Registered via the API; SWF-tag wiring is separate/TODO. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/GradientGlowFilter.hx | 357 +++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 src/openfl/filters/GradientGlowFilter.hx diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx new file mode 100644 index 0000000000..a7a1af1408 --- /dev/null +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -0,0 +1,357 @@ +package openfl.filters; + +#if !flash +import openfl.display.BitmapData; +import openfl.display.DisplayObjectRenderer; +import openfl.display.Shader; +import openfl.geom.Point; +import openfl.geom.Rectangle; + +/** + The GradientGlowFilter class lets you apply a gradient glow effect to + display objects. It is a glow whose colour is taken from a gradient (defined + by `colors`/`alphas`/`ratios`) instead of a single colour: `ratios` position + the colours along the glow — 0 is the outermost point, 255 the innermost. + + Not present in stock OpenFL; implemented here for the non-flash targets by + blurring the object's alpha into a distance field and indexing a 256-entry + ramp built from the stops. +**/ +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(openfl.filters.BlurFilter) +@:access(openfl.geom.Point) +@:access(openfl.geom.Rectangle) +@:final class GradientGlowFilter extends BitmapFilter +{ + @:noCompletion private static var __gradientShader = new GradientGlowShader(); + + public var distance(get, set):Float; + public var angle(get, set):Float; + public var colors(get, set):Array; + public var alphas(get, set):Array; + public var ratios(get, set):Array; + public var blurX(get, set):Float; + public var blurY(get, set):Float; + public var strength(get, set):Float; + public var quality(get, set):Int; + public var type(get, set):BitmapFilterType; + public var knockout(get, set):Bool; + + @:noCompletion private var __distance:Float; + @:noCompletion private var __angle:Float; + @:noCompletion private var __colors:Array; + @:noCompletion private var __alphas:Array; + @:noCompletion private var __ratios:Array; + @:noCompletion private var __blurX:Float; + @:noCompletion private var __blurY:Float; + @:noCompletion private var __strength:Float; + @:noCompletion private var __quality:Int; + @:noCompletion private var __type:BitmapFilterType; + @:noCompletion private var __knockout:Bool; + @:noCompletion private var __offsetX:Int; + @:noCompletion private var __offsetY:Int; + @:noCompletion private var __horizontalPasses:Int; + @:noCompletion private var __verticalPasses:Int; + @:noCompletion private var __ramp:BitmapData; + @:noCompletion private var __rampDirty:Bool; + + public function new(distance:Float = 4, angle:Float = 45, colors:Array = null, alphas:Array = null, ratios:Array = null, + blurX:Float = 4, blurY:Float = 4, strength:Float = 1, quality:Int = 1, type:BitmapFilterType = OUTER, knockout:Bool = false) + { + super(); + + __distance = distance; + __angle = angle; + __colors = (colors != null) ? colors : [0xFFFFFF, 0xFFFFFF]; + __alphas = (alphas != null) ? alphas : [0, 1]; + __ratios = (ratios != null) ? ratios : [0, 255]; + __blurX = blurX; + __blurY = blurY; + __strength = strength; + __quality = quality; + __type = type; + __knockout = knockout; + __rampDirty = true; + + __needSecondBitmapData = true; + __preserveObject = true; + __renderDirty = true; + + __updateSize(); + } + + public override function clone():BitmapFilter + { + return new GradientGlowFilter(__distance, __angle, __colors.copy(), __alphas.copy(), __ratios.copy(), __blurX, __blurY, __strength, __quality, + __type, __knockout); + } + + @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData + { + // software path not implemented yet (GL shader path below is the one used + // on-screen); return the source unchanged so nothing crashes. + return sourceBitmapData; + } + + @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader + { + #if !macro + var numBlurPasses = __horizontalPasses + __verticalPasses; + if (pass < numBlurPasses) + { + // blur the object's alpha into a soft distance field (reuse BlurFilter) + var horizontal = pass < __horizontalPasses; + #if flash_box_blur + return BlurFilter.__setupBoxBlur(horizontal, horizontal ? __blurX : __blurY); + #else + var shader = BlurFilter.__blurShader; + if (horizontal) + { + var scale = Math.pow(0.5, pass >> 1); + shader.uRadius.value[0] = __blurX * scale; + shader.uRadius.value[1] = 0; + } + else + { + var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); + shader.uRadius.value[0] = 0; + shader.uRadius.value[1] = __blurY * scale; + } + return shader; + #end + } + + if (__rampDirty) __buildRamp(); + + var shader = __gradientShader; + shader.sourceBitmap.input = sourceBitmapData; + shader.gradientRamp.input = __ramp; + shader.offset.value[0] = __offsetX; + shader.offset.value[1] = __offsetY; + shader.uStrength.value[0] = __strength; + shader.uInner.value[0] = (__type == INNER) ? 1.0 : 0.0; + shader.uFull.value[0] = (__type == FULL) ? 1.0 : 0.0; + shader.uKnockout.value[0] = __knockout ? 1.0 : 0.0; + return shader; + #else + return null; + #end + } + + // Build the 256x1 straight-ARGB ramp from (colors, alphas, ratios). + @:noCompletion private function __buildRamp():Void + { + if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); + var n = __colors.length; + var si = 0; + for (i in 0...256) + { + while (si < n - 1 && __ratios[si + 1] < i) + si++; + var r0 = __ratios[si]; + var c0 = __colors[si]; + var a0 = __alphas[si]; + var rr:Float, gg:Float, bb:Float, aa:Float; + if (si >= n - 1 || i <= r0) + { + rr = (c0 >> 16) & 0xFF; + gg = (c0 >> 8) & 0xFF; + bb = c0 & 0xFF; + aa = a0 * 255; + } + else + { + var r1 = __ratios[si + 1]; + var c1 = __colors[si + 1]; + var a1 = __alphas[si + 1]; + var f = (r1 > r0) ? (i - r0) / (r1 - r0) : 0.0; + rr = ((c0 >> 16) & 0xFF) + (((c1 >> 16) & 0xFF) - ((c0 >> 16) & 0xFF)) * f; + gg = ((c0 >> 8) & 0xFF) + (((c1 >> 8) & 0xFF) - ((c0 >> 8) & 0xFF)) * f; + bb = (c0 & 0xFF) + ((c1 & 0xFF) - (c0 & 0xFF)) * f; + aa = (a0 + (a1 - a0) * f) * 255; + } + var col = (Std.int(aa) << 24) | (Std.int(rr) << 16) | (Std.int(gg) << 8) | Std.int(bb); + __ramp.setPixel32(i, 0, col); + } + __rampDirty = false; + } + + @:noCompletion private function __updateSize():Void + { + __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); + __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); + __topExtension = Math.ceil((__offsetY < 0 ? -__offsetY : 0) + __blurY * 1.5); + __bottomExtension = Math.ceil((__offsetY > 0 ? __offsetY : 0) + __blurY * 1.5); + __leftExtension = Math.ceil((__offsetX < 0 ? -__offsetX : 0) + __blurX * 1.5); + __rightExtension = Math.ceil((__offsetX > 0 ? __offsetX : 0) + __blurX * 1.5); + + #if flash_box_blur + var q = (__quality > 0) ? __quality : 1; + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; + #else + __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; + __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; + #end + __numShaderPasses = __horizontalPasses + __verticalPasses + 1; + } + + // Getters / setters + @:noCompletion private function get_distance():Float return __distance; + + @:noCompletion private function set_distance(v:Float):Float + { + if (v != __distance) { __distance = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_angle():Float return __angle; + + @:noCompletion private function set_angle(v:Float):Float + { + if (v != __angle) { __angle = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_colors():Array return __colors; + + @:noCompletion private function set_colors(v:Array):Array + { + __colors = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_alphas():Array return __alphas; + + @:noCompletion private function set_alphas(v:Array):Array + { + __alphas = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_ratios():Array return __ratios; + + @:noCompletion private function set_ratios(v:Array):Array + { + __ratios = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_blurX():Float return __blurX; + + @:noCompletion private function set_blurX(v:Float):Float + { + if (v != __blurX) { __blurX = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_blurY():Float return __blurY; + + @:noCompletion private function set_blurY(v:Float):Float + { + if (v != __blurY) { __blurY = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_strength():Float return __strength; + + @:noCompletion private function set_strength(v:Float):Float + { + if (v != __strength) { __strength = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_quality():Int return __quality; + + @:noCompletion private function set_quality(v:Int):Int + { + if (v != __quality) { __quality = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_type():BitmapFilterType return __type; + + @:noCompletion private function set_type(v:BitmapFilterType):BitmapFilterType + { + if (v != __type) { __type = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_knockout():Bool return __knockout; + + @:noCompletion private function set_knockout(v:Bool):Bool + { + if (v != __knockout) { __knockout = v; __renderDirty = true; } + return v; + } +} + +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +private class GradientGlowShader extends BitmapFilterShader +{ + @:glFragmentSource(" + uniform sampler2D openfl_Texture; + uniform sampler2D sourceBitmap; + uniform sampler2D gradientRamp; + uniform float uStrength; + uniform float uInner; + uniform float uFull; + uniform float uKnockout; + varying vec4 textureCoords; + + void main(void) { + vec4 src = texture2D(sourceBitmap, textureCoords.xy); + float field = texture2D(openfl_Texture, textureCoords.zw).a; + float f = clamp(field * uStrength, 0.0, 1.0); + + // index the ramp by the distance field (high near the shape = inner + // ratio 255, low far away = outer ratio 0), same for every type. + vec4 g = texture2D(gradientRamp, vec2(f, 0.5)); + + if (uInner > 0.5) { + vec4 inner = g * src.a; + if (uKnockout > 0.5) gl_FragColor = inner; + else gl_FragColor = src * (1.0 - inner.a) + inner; + } else if (uFull > 0.5) { + if (uKnockout > 0.5) gl_FragColor = g; + else gl_FragColor = src * (1.0 - g.a) + g; + } else { + vec4 outer = g * (1.0 - src.a); + if (uKnockout > 0.5) gl_FragColor = outer; + else gl_FragColor = src + outer; + } + } + ") + @:glVertexSource("attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 openfl_TextureSize; + uniform vec2 offset; + varying vec4 textureCoords; + + void main(void) { + gl_Position = openfl_Matrix * openfl_Position; + textureCoords = vec4(openfl_TextureCoord, openfl_TextureCoord - offset / openfl_TextureSize); + } + ") + public function new() + { + super(); + #if !macro + offset.value = [0, 0]; + uStrength.value = [1]; + uInner.value = [0]; + uFull.value = [0]; + uKnockout.value = [0]; + #end + } +} +#else +typedef GradientGlowFilter = flash.filters.GradientGlowFilter; +#end From 82366e13da34925d9e05f9e42732366f3a8e0453 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Mon, 31 Aug 2026 22:01:58 +0200 Subject: [PATCH 04/21] filters: add GradientBevelFilter (missing from OpenFL) The second missing gradient filter (in the Flash IDE menu; SWF parser flagged "not supported on native"; no Ruffle reference). Matched against Flash/AIR empirically. Implementation (GL shader path): blur the object's alpha (reusing BlurFilter's box blur under -Dflash_box_blur, else the Gaussian), then a shader samples the blurred field at +/- the bevel offset (distance,angle) to form a signed distance field (blurLeft - blurRight)*strength, mapped into a 256-entry ramp (ratio 0 = one edge, 128 = base/transparent, 255 = other edge) built from (colors, alphas, ratios). Composites per type inner/outer/full with knockout, same math as BevelFilter but the ramp replaces the highlight/shadow colours. Falls back to the flash.filters typedef on the flash target. Verified vs Flash/AIR on a blue->transparent->white ramp: inner, outer, full and strength all match. Software __applyFilter is a placeholder (GL path renders on-screen). API-registered; SWF-tag wiring is separate/TODO. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/GradientBevelFilter.hx | 362 ++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/openfl/filters/GradientBevelFilter.hx diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx new file mode 100644 index 0000000000..f24088b4bb --- /dev/null +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -0,0 +1,362 @@ +package openfl.filters; + +#if !flash +import openfl.display.BitmapData; +import openfl.display.DisplayObjectRenderer; +import openfl.display.Shader; +import openfl.geom.Point; +import openfl.geom.Rectangle; + +/** + The GradientBevelFilter class lets you apply a gradient bevel effect to + display objects. The bevel's colours come from a gradient (defined by + `colors`/`alphas`/`ratios`) instead of separate highlight/shadow colours: + ratio 0 is one edge, 255 the other, and 128 is the base (usually + transparent), which appears where there is no bevel. + + Not present in stock OpenFL; implemented here for the non-flash targets by + sampling the blurred alpha at +/- the bevel offset to build a signed + distance field and indexing a 256-entry ramp built from the stops. +**/ +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +@:access(openfl.filters.BlurFilter) +@:access(openfl.geom.Point) +@:access(openfl.geom.Rectangle) +@:final class GradientBevelFilter extends BitmapFilter +{ + @:noCompletion private static var __gradientShader = new GradientBevelShader(); + + public var distance(get, set):Float; + public var angle(get, set):Float; + public var colors(get, set):Array; + public var alphas(get, set):Array; + public var ratios(get, set):Array; + public var blurX(get, set):Float; + public var blurY(get, set):Float; + public var strength(get, set):Float; + public var quality(get, set):Int; + public var type(get, set):BitmapFilterType; + public var knockout(get, set):Bool; + + @:noCompletion private var __distance:Float; + @:noCompletion private var __angle:Float; + @:noCompletion private var __colors:Array; + @:noCompletion private var __alphas:Array; + @:noCompletion private var __ratios:Array; + @:noCompletion private var __blurX:Float; + @:noCompletion private var __blurY:Float; + @:noCompletion private var __strength:Float; + @:noCompletion private var __quality:Int; + @:noCompletion private var __type:BitmapFilterType; + @:noCompletion private var __knockout:Bool; + @:noCompletion private var __horizontalPasses:Int; + @:noCompletion private var __verticalPasses:Int; + @:noCompletion private var __ramp:BitmapData; + @:noCompletion private var __rampDirty:Bool; + + public function new(distance:Float = 4, angle:Float = 45, colors:Array = null, alphas:Array = null, ratios:Array = null, + blurX:Float = 4, blurY:Float = 4, strength:Float = 1, quality:Int = 1, type:BitmapFilterType = INNER, knockout:Bool = false) + { + super(); + + __distance = distance; + __angle = angle; + __colors = (colors != null) ? colors : [0xFFFFFF, 0x808080, 0x000000]; + __alphas = (alphas != null) ? alphas : [1, 0, 1]; + __ratios = (ratios != null) ? ratios : [0, 128, 255]; + __blurX = blurX; + __blurY = blurY; + __strength = strength; + __quality = quality; + __type = type; + __knockout = knockout; + __rampDirty = true; + + __needSecondBitmapData = true; + __preserveObject = true; + __renderDirty = true; + + __updateSize(); + } + + public override function clone():BitmapFilter + { + return new GradientBevelFilter(__distance, __angle, __colors.copy(), __alphas.copy(), __ratios.copy(), __blurX, __blurY, __strength, __quality, + __type, __knockout); + } + + @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData + { + // software path not implemented yet (GL shader path below is used on-screen) + return sourceBitmapData; + } + + @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader + { + #if !macro + var numBlurPasses = __horizontalPasses + __verticalPasses; + if (pass < numBlurPasses) + { + var horizontal = pass < __horizontalPasses; + #if flash_box_blur + return BlurFilter.__setupBoxBlur(horizontal, horizontal ? __blurX : __blurY); + #else + var shader = BlurFilter.__blurShader; + if (horizontal) + { + var scale = Math.pow(0.5, pass >> 1); + shader.uRadius.value[0] = __blurX * scale; + shader.uRadius.value[1] = 0; + } + else + { + var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); + shader.uRadius.value[0] = 0; + shader.uRadius.value[1] = __blurY * scale; + } + return shader; + #end + } + + if (__rampDirty) __buildRamp(); + + var rad = __angle * Math.PI / 180; + var shader = __gradientShader; + shader.sourceBitmap.input = sourceBitmapData; + shader.gradientRamp.input = __ramp; + shader.uTransformX.value[0] = __distance * Math.cos(rad); + shader.uTransformY.value[0] = __distance * Math.sin(rad); + shader.uStrength.value[0] = __strength; + shader.uBevelType.value[0] = (__type == INNER) ? 0.0 : (__type == OUTER ? 1.0 : 2.0); + shader.uKnockout.value[0] = __knockout; + return shader; + #else + return null; + #end + } + + // Build the 256x1 straight-ARGB ramp from (colors, alphas, ratios). + @:noCompletion private function __buildRamp():Void + { + if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); + var n = __colors.length; + var si = 0; + for (i in 0...256) + { + while (si < n - 1 && __ratios[si + 1] < i) + si++; + var r0 = __ratios[si]; + var c0 = __colors[si]; + var a0 = __alphas[si]; + var rr:Float, gg:Float, bb:Float, aa:Float; + if (si >= n - 1 || i <= r0) + { + rr = (c0 >> 16) & 0xFF; + gg = (c0 >> 8) & 0xFF; + bb = c0 & 0xFF; + aa = a0 * 255; + } + else + { + var r1 = __ratios[si + 1]; + var c1 = __colors[si + 1]; + var a1 = __alphas[si + 1]; + var f = (r1 > r0) ? (i - r0) / (r1 - r0) : 0.0; + rr = ((c0 >> 16) & 0xFF) + (((c1 >> 16) & 0xFF) - ((c0 >> 16) & 0xFF)) * f; + gg = ((c0 >> 8) & 0xFF) + (((c1 >> 8) & 0xFF) - ((c0 >> 8) & 0xFF)) * f; + bb = (c0 & 0xFF) + ((c1 & 0xFF) - (c0 & 0xFF)) * f; + aa = (a0 + (a1 - a0) * f) * 255; + } + var col = (Std.int(aa) << 24) | (Std.int(rr) << 16) | (Std.int(gg) << 8) | Std.int(bb); + __ramp.setPixel32(i, 0, col); + } + __rampDirty = false; + } + + @:noCompletion private function __updateSize():Void + { + var d = (__distance < 0 ? -__distance : __distance); + __leftExtension = __rightExtension = Math.ceil(__blurX * 1.5 + d); + __topExtension = __bottomExtension = Math.ceil(__blurY * 1.5 + d); + + #if flash_box_blur + var q = (__quality > 0) ? __quality : 1; + __horizontalPasses = (__blurX <= 0) ? 0 : q; + __verticalPasses = (__blurY <= 0) ? 0 : q; + #else + __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; + __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; + #end + __numShaderPasses = __horizontalPasses + __verticalPasses + 1; + } + + // Getters / setters + @:noCompletion private function get_distance():Float return __distance; + + @:noCompletion private function set_distance(v:Float):Float + { + if (v != __distance) { __distance = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_angle():Float return __angle; + + @:noCompletion private function set_angle(v:Float):Float + { + if (v != __angle) { __angle = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_colors():Array return __colors; + + @:noCompletion private function set_colors(v:Array):Array + { + __colors = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_alphas():Array return __alphas; + + @:noCompletion private function set_alphas(v:Array):Array + { + __alphas = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_ratios():Array return __ratios; + + @:noCompletion private function set_ratios(v:Array):Array + { + __ratios = v; __rampDirty = true; __renderDirty = true; + return v; + } + + @:noCompletion private function get_blurX():Float return __blurX; + + @:noCompletion private function set_blurX(v:Float):Float + { + if (v != __blurX) { __blurX = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_blurY():Float return __blurY; + + @:noCompletion private function set_blurY(v:Float):Float + { + if (v != __blurY) { __blurY = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_strength():Float return __strength; + + @:noCompletion private function set_strength(v:Float):Float + { + if (v != __strength) { __strength = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_quality():Int return __quality; + + @:noCompletion private function set_quality(v:Int):Int + { + if (v != __quality) { __quality = v; __updateSize(); __renderDirty = true; } + return v; + } + + @:noCompletion private function get_type():BitmapFilterType return __type; + + @:noCompletion private function set_type(v:BitmapFilterType):BitmapFilterType + { + if (v != __type) { __type = v; __renderDirty = true; } + return v; + } + + @:noCompletion private function get_knockout():Bool return __knockout; + + @:noCompletion private function set_knockout(v:Bool):Bool + { + if (v != __knockout) { __knockout = v; __renderDirty = true; } + return v; + } +} + +#if !openfl_debug +@:fileXml('tags="haxe,release"') +@:noDebug +#end +private class GradientBevelShader extends BitmapFilterShader +{ + @:glFragmentSource("uniform sampler2D openfl_Texture; + uniform sampler2D sourceBitmap; + uniform sampler2D gradientRamp; + uniform float uBevelType; + uniform bool uKnockout; + uniform float uStrength; + varying vec2 vTextureCoord; + varying vec2 vTransform; + + void main(void) { + vec4 dest = texture2D(sourceBitmap, vTextureCoord); + vec2 uvL = vTextureCoord + vTransform; + vec2 uvR = vTextureCoord - vTransform; + float bL = texture2D(openfl_Texture, uvL).a; + float bR = texture2D(openfl_Texture, uvR).a; + if (uvL.x<0.0||uvL.x>1.0||uvL.y<0.0||uvL.y>1.0) bL = 0.0; + if (uvR.x<0.0||uvR.x>1.0||uvR.y<0.0||uvR.y>1.0) bR = 0.0; + + // signed distance field -> ramp index (-1 = one edge/ratio 0, + // 0 = base/ratio 128, +1 = other edge/ratio 255) + float sd = clamp((bL - bR) * uStrength, -1.0, 1.0); + vec4 glow = texture2D(gradientRamp, vec2(sd * 0.5 + 0.5, 0.5)); + + if (uBevelType == 0.0) { + if (uKnockout) gl_FragColor = glow * dest.a; + else gl_FragColor = glow * dest.a + dest * (1.0 - glow.a); + } else if (uBevelType == 1.0) { + if (uKnockout) gl_FragColor = glow - glow * dest.a; + else gl_FragColor = dest + glow - glow * dest.a; + } else { + if (uKnockout) gl_FragColor = glow; + else gl_FragColor = dest - dest * glow.a + glow; + } + }") + @:glVertexSource("attribute vec4 openfl_Position; + attribute vec2 openfl_TextureCoord; + uniform mat4 openfl_Matrix; + uniform vec2 uTextureSize; + uniform float uTransformX; + uniform float uTransformY; + varying vec2 vTextureCoord; + varying vec2 vTransform; + + void main(void) { + gl_Position = openfl_Matrix * openfl_Position; + vTextureCoord = openfl_TextureCoord; + vTransform = vec2(uTransformX / uTextureSize.x, uTransformY / uTextureSize.y); + }") + public function new() + { + super(); + #if !macro + uTransformX.value = [0]; + uTransformY.value = [0]; + uBevelType.value = [0.0]; + uKnockout.value = [false]; + uStrength.value = [1]; + #end + } + + @:noCompletion private override function __update():Void + { + #if !macro + uTextureSize.value = [__texture.input.width, __texture.input.height]; + #end + super.__update(); + } +} +#else +typedef GradientBevelFilter = flash.filters.GradientBevelFilter; +#end From 564bff53fba0fd0741fa0260391fff94e17b080b Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Mon, 31 Aug 2026 23:42:51 +0200 Subject: [PATCH 05/21] Scale filter extension with quality to fix box-blur clip At high quality the box-blur path (-Dflash_box_blur) applies `quality` passes, each widening the shadow/glow/bevel support by ~half the blur, so the effective reach is ~quality*blur/2 per side. __updateSize() only reserved one blur radius, so the effect was hard-clipped to a rectangle at the cache texture bounds (e.g. quality=15, blur=16) while Flash/AIR radiate smoothly. Reserve ceil(blur*0.5*quality)+4 per side under #if flash_box_blur across DropShadow / Glow / Bevel / GradientGlow / GradientBevel; the Gaussian #else path keeps its original formula for A/B testing. Also make set_quality on DropShadow/Glow/Bevel recompute the extension (update __quality then call __updateSize) so post-construction quality changes resize correctly. Verified against the Flash/AIR baseline via a parameter sweep: the quality cell now matches, and strength/distance/blur/bevel+DS all track the baseline. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BevelFilter.hx | 20 +++++++++++++---- src/openfl/filters/DropShadowFilter.hx | 27 ++++++++++++++++++----- src/openfl/filters/GlowFilter.hx | 13 +++++++++-- src/openfl/filters/GradientBevelFilter.hx | 8 +++++++ src/openfl/filters/GradientGlowFilter.hx | 18 +++++++++++---- 5 files changed, 71 insertions(+), 15 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 3e963520d7..42b7f1f029 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -362,6 +362,8 @@ import lime._internal.graphics.ImageDataUtil; __numShaderPasses = __horizontalPasses + __verticalPasses + 1; if (value != __quality) __renderDirty = true; + __quality = value; + __updateSize(); // extension depends on quality (box-blur reach) return __quality = value; } @@ -455,10 +457,20 @@ import lime._internal.graphics.ImageDataUtil; { var offsetX:Int = __type != "inner" ? Math.ceil(__distance * Math.cos(__angle * Math.PI / 180)) : 0; var offsetY:Int = __type != "inner" ? Math.ceil(__distance * Math.sin(__angle * Math.PI / 180)) : 0; - __topExtension = Math.ceil((offsetY < 0 ? -offsetY : 0) + __blurY); - __bottomExtension = Math.ceil((offsetY > 0 ? offsetY : 0) + __blurY); - __leftExtension = Math.ceil((offsetX < 0 ? -offsetX : 0) + __blurX); - __rightExtension = Math.ceil((offsetX > 0 ? offsetX : 0) + __blurX); + #if flash_box_blur + // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + // reserve the full spread so the bevel isn't clipped at high quality. + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q) + 4; + var exY = Math.ceil(__blurY * 0.5 * q) + 4; + #else + var exX = Math.ceil(__blurX); + var exY = Math.ceil(__blurY); + #end + __topExtension = (offsetY < 0 ? -offsetY : 0) + exY; + __bottomExtension = (offsetY > 0 ? offsetY : 0) + exY; + __leftExtension = (offsetX < 0 ? -offsetX : 0) + exX; + __rightExtension = (offsetX > 0 ? offsetX : 0) + exX; } } diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index b8e3f88d49..fb023e4846 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -399,10 +399,22 @@ import lime._internal.graphics.ImageDataUtil; // TODO { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - __topExtension = Math.ceil((__offsetY < 0 ? -__offsetY : 0) + __blurY); - __bottomExtension = Math.ceil((__offsetY > 0 ? __offsetY : 0) + __blurY); - __leftExtension = Math.ceil((__offsetX < 0 ? -__offsetX : 0) + __blurX); - __rightExtension = Math.ceil((__offsetX > 0 ? __offsetX : 0) + __blurX); + #if flash_box_blur + // Box blur applies `quality` passes; each pass widens the shadow's + // support by ~half the blur, so the reach grows to ~quality*blur/2. If we + // only reserve one blur radius the shadow is hard-clipped to a rectangle + // at high quality (Flash reserves room for the full spread). + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q) + 4; + var exY = Math.ceil(__blurY * 0.5 * q) + 4; + #else + var exX = Math.ceil(__blurX); + var exY = Math.ceil(__blurY); + #end + __topExtension = Std.int((__offsetY < 0 ? -__offsetY : 0) + exY); + __bottomExtension = Std.int((__offsetY > 0 ? __offsetY : 0) + exY); + __leftExtension = Std.int((__offsetX < 0 ? -__offsetX : 0) + exX); + __rightExtension = Std.int((__offsetX > 0 ? __offsetX : 0) + exX); __calculateNumShaderPasses(); } @@ -549,7 +561,12 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function set_quality(value:Int):Int { - if (value != __quality) __renderDirty = true; + if (value != __quality) + { + __renderDirty = true; + __quality = value; + __updateSize(); // passes & extension both depend on quality + } return __quality = value; } diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index cb1298baec..3b1e5118db 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -356,9 +356,17 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __updateSize():Void { + #if flash_box_blur + // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + // reserve the full spread so the glow isn't clipped at high quality. + var q = (__quality > 0) ? __quality : 1; + __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 0.5 * q) + 4 : 0); + __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 0.5 * q) + 4 : 0); + #else __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 1.5) : 0); - __rightExtension = __leftExtension; __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 1.5) : 0); + #end + __rightExtension = __leftExtension; __bottomExtension = __topExtension; __calculateNumShaderPasses(); } @@ -512,7 +520,8 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (value != __quality) { __renderDirty = true; - __calculateNumShaderPasses(); + __quality = value; + __updateSize(); // passes & extension both depend on quality } return __quality = value; } diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index f24088b4bb..67091833db 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -179,8 +179,16 @@ import openfl.geom.Rectangle; @:noCompletion private function __updateSize():Void { var d = (__distance < 0 ? -__distance : __distance); + #if flash_box_blur + // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + // reserve the full spread so the gradient bevel isn't clipped at high quality. + var qext = (__quality > 0) ? __quality : 1; + __leftExtension = __rightExtension = Math.ceil(__blurX * 0.5 * qext + d) + 4; + __topExtension = __bottomExtension = Math.ceil(__blurY * 0.5 * qext + d) + 4; + #else __leftExtension = __rightExtension = Math.ceil(__blurX * 1.5 + d); __topExtension = __bottomExtension = Math.ceil(__blurY * 1.5 + d); + #end #if flash_box_blur var q = (__quality > 0) ? __quality : 1; diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index a7a1af1408..7df9a34a23 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -183,10 +183,20 @@ import openfl.geom.Rectangle; { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - __topExtension = Math.ceil((__offsetY < 0 ? -__offsetY : 0) + __blurY * 1.5); - __bottomExtension = Math.ceil((__offsetY > 0 ? __offsetY : 0) + __blurY * 1.5); - __leftExtension = Math.ceil((__offsetX < 0 ? -__offsetX : 0) + __blurX * 1.5); - __rightExtension = Math.ceil((__offsetX > 0 ? __offsetX : 0) + __blurX * 1.5); + #if flash_box_blur + // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + // reserve the full spread so the gradient glow isn't clipped at high quality. + var qext = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * qext) + 4; + var exY = Math.ceil(__blurY * 0.5 * qext) + 4; + #else + var exX = Math.ceil(__blurX * 1.5); + var exY = Math.ceil(__blurY * 1.5); + #end + __topExtension = (__offsetY < 0 ? -__offsetY : 0) + exY; + __bottomExtension = (__offsetY > 0 ? __offsetY : 0) + exY; + __leftExtension = (__offsetX < 0 ? -__offsetX : 0) + exX; + __rightExtension = (__offsetX > 0 ? __offsetX : 0) + exX; #if flash_box_blur var q = (__quality > 0) ? __quality : 1; From 7908c6739b0c5aa2854720b84a9df6e154e0cbd8 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Tue, 1 Sep 2026 00:20:55 +0200 Subject: [PATCH 06/21] GradientBevel: extension = true bevel extent, fixing over-wide band The gradient bevel's coloured band rendered ~59% wider than Flash/AIR (per-cell mean|diff| ~17 on outer/full/strength). Cause: because the ramp's middle stop is opaque, flat regions map to the middle colour, so the visible band edge is the cache-texture boundary -- and __updateSize reserved ceil(blur*0.5*q + distance) + 4, whose +4 safety margin showed up as extra opaque fill past where Flash draws. Flash's band stops exactly at the box-blur support (quality*blur/2) plus the transform offset (distance) -- the extent where the field bL-bR is non-zero (verified by scanline: FLASH edge at 11px = 8 + 2.83). Set the extension to that true extent, mirroring BevelFilter's asymmetric offset structure but without the +4 margin (BevelFilter can afford it because its band fades to transparent; the gradient band is opaque). Offset magnitude is ceil(abs(distance*cos/sin)) so negative angles don't lose a pixel. Removes the magic +4; adds no new constant (q*blur/2 is the box-blur support, per the Change-5 derivation). Verified vs AIR: band start 199->199, outer-band yellow area 4121->2596 px (= Flash 2596), mean|diff| 17->~1 across inner / outer / full / angle / strength. GradientGlow left as-is (already within tolerance: cyan centre pixel-identical, only sub-pixel transition stretch). Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/GradientBevelFilter.hx | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index 67091833db..b600e92751 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -178,17 +178,29 @@ import openfl.geom.Rectangle; @:noCompletion private function __updateSize():Void { - var d = (__distance < 0 ? -__distance : __distance); + // The bevel field (bL-bR) is exactly zero beyond the box-blur support + // (quality*blur/2) offset by the transform (distance). Because the ramp's + // middle stop is usually opaque, flat regions map to the middle colour and + // the *visible* band edge sits at the texture boundary — so the extension + // must equal the true bevel extent (support + directional offset), with no + // safety margin, or the middle colour over-fills past where Flash stops. + // This mirrors BevelFilter's asymmetric extension (which matches Flash), + // minus the margin that filter can afford only because its band fades out. + var rad = __angle * Math.PI / 180; + // magnitude of the transform offset per axis (band reaches support + |offset| + // on every side); ceil the absolute value so negative angles don't lose a pixel + var offsetX:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.cos(rad))) : 0; + var offsetY:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.sin(rad))) : 0; #if flash_box_blur - // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); - // reserve the full spread so the gradient bevel isn't clipped at high quality. var qext = (__quality > 0) ? __quality : 1; - __leftExtension = __rightExtension = Math.ceil(__blurX * 0.5 * qext + d) + 4; - __topExtension = __bottomExtension = Math.ceil(__blurY * 0.5 * qext + d) + 4; + var exX = Math.ceil(__blurX * 0.5 * qext); + var exY = Math.ceil(__blurY * 0.5 * qext); #else - __leftExtension = __rightExtension = Math.ceil(__blurX * 1.5 + d); - __topExtension = __bottomExtension = Math.ceil(__blurY * 1.5 + d); + var exX = Math.ceil(__blurX * 1.5); + var exY = Math.ceil(__blurY * 1.5); #end + __leftExtension = __rightExtension = exX + offsetX; + __topExtension = __bottomExtension = exY + offsetY; #if flash_box_blur var q = (__quality > 0) ? __quality : 1; From 93aa2e2793e0da1bd1a3ffc261415ad8f54ab940 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Tue, 1 Sep 2026 18:41:30 +0200 Subject: [PATCH 07/21] Make Flash-faithful box blur the default (remove -Dflash_box_blur gate) The box blur has been validated against the AIR baseline across the full parameter sweep (strength/distance/blur/quality, bevel/glow/gradient/stacks), so the -Dflash_box_blur conditional-compilation gate is no longer needed. This removes the flag from all six blur-based filters (Blur, Glow, DropShadow, Bevel, GradientGlow, GradientBevel): the box-blur branch becomes the only path and the old fixed-tap Gaussian #else branches are deleted, along with the now -dead shader classes BlurShader (BlurFilter) and BlurAlphaShader (GlowFilter). Filters now render correctly with a plain build, no define required. Verified: the flag-free build is pixel-identical to the previous -Dflash_box_blur build (mean|diff| = 0.0) and matches AIR in the same band as before. Net -326 lines. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BevelFilter.hx | 27 ----- src/openfl/filters/BlurFilter.hx | 103 +------------------ src/openfl/filters/DropShadowFilter.hx | 32 ------ src/openfl/filters/GlowFilter.hx | 114 +--------------------- src/openfl/filters/GradientBevelFilter.hx | 34 +------ src/openfl/filters/GradientGlowFilter.hx | 34 +------ 6 files changed, 9 insertions(+), 335 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 42b7f1f029..b786ffea1d 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -173,25 +173,8 @@ import lime._internal.graphics.ImageDataUtil; var numBlurPasses = __horizontalPasses + __verticalPasses; if (blurPass < numBlurPasses) { - #if flash_box_blur var horizontal = pass < __horizontalPasses; return BlurFilter.__setupBoxBlur(horizontal, horizontal ? blurX : blurY); - #else - var shader = BlurFilter.__blurShader; - if (pass < __horizontalPasses) - { - var scale = Math.pow(0.5, pass >> 1); - shader.uRadius.value[0] = blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = blurY * scale; - } - return shader; - #end } __bevelShader.sourceBitmap.input = sourceBitmapData; @@ -351,13 +334,8 @@ import lime._internal.graphics.ImageDataUtil; value = value < 1 ? 1 : value; value = value > 15 ? 15 : value; - #if flash_box_blur __horizontalPasses = (__blurX <= 0) ? 0 : value; __verticalPasses = (__blurY <= 0) ? 0 : value; - #else - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (value / 4)); - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (value / 4)); - #end __numShaderPasses = __horizontalPasses + __verticalPasses + 1; @@ -457,16 +435,11 @@ import lime._internal.graphics.ImageDataUtil; { var offsetX:Int = __type != "inner" ? Math.ceil(__distance * Math.cos(__angle * Math.PI / 180)) : 0; var offsetY:Int = __type != "inner" ? Math.ceil(__distance * Math.sin(__angle * Math.PI / 180)) : 0; - #if flash_box_blur // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); // reserve the full spread so the bevel isn't clipped at high quality. var q = (__quality > 0) ? __quality : 1; var exX = Math.ceil(__blurX * 0.5 * q) + 4; var exY = Math.ceil(__blurY * 0.5 * q) + 4; - #else - var exX = Math.ceil(__blurX); - var exY = Math.ceil(__blurY); - #end __topExtension = (offsetY < 0 ? -offsetY : 0) + exY; __bottomExtension = (offsetY > 0 ? offsetY : 0) + exY; __leftExtension = (offsetX < 0 ? -offsetX : 0) + exX; diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index feabf0bbe3..3a0b03c34f 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -69,12 +69,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:access(openfl.geom.Rectangle) @:final class BlurFilter extends BitmapFilter { - @:noCompletion private static var __blurShader:BlurShader = new BlurShader(); - #if flash_box_blur - // Flash-faithful fractional box blur (Ruffle-style), gated behind - // -Dflash_box_blur. Default build keeps the 7-tap Gaussian above. + // Flash-faithful fractional box blur (Ruffle-style). Shared by the bevel and + // gradient filters, which blur the source alpha before deriving their effect. @:noCompletion private static var __boxBlurShader:BoxBlurShader = new BoxBlurShader(); - #end /** The amount of horizontal blur. Valid values are from 0 to 255(floating @@ -199,7 +196,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader { - #if flash_box_blur #if !macro // passes alternate horizontal / vertical; each applies one full box blur // for its axis, iterated `quality` times (separable box passes commute) @@ -208,27 +204,8 @@ import lime._internal.graphics.ImageDataUtil; // TODO #else return __boxBlurShader; #end - #else - #if !macro - if (pass < __horizontalPasses) - { - var scale = Math.pow(0.5, pass >> 1); - __blurShader.uRadius.value[0] = blurX * scale; - __blurShader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); - __blurShader.uRadius.value[0] = 0; - __blurShader.uRadius.value[1] = blurY * scale; - } - #end - - return __blurShader; - #end } - #if flash_box_blur // Configure the shared box-blur shader for one axis of one pass. Reused by // BevelFilter (which blurs the source before deriving highlight/shadow). @:noCompletion private static function __setupBoxBlur(horizontal:Bool, v:Float):BitmapFilterShader @@ -262,7 +239,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO } return s; } - #end @:noCompletion inline function __padFor(value:Float):Int { @@ -324,18 +300,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO { // TODO: Quality effect with fewer passes? - #if flash_box_blur // one horizontal + one vertical box pass per quality iteration var passes = (value > 0) ? value : 1; __horizontalPasses = passes; __verticalPasses = passes; __numShaderPasses = passes * 2; - #else - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (value / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (value / 4)) + 1; - - __numShaderPasses = __horizontalPasses + __verticalPasses; - #end if (value != __quality) __renderDirty = true; __quality = value; @@ -345,73 +314,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO } } -#if !openfl_debug -@:fileXml('tags="haxe,release"') -@:noDebug -#end -private class BlurShader extends BitmapFilterShader -{ - @:glFragmentSource("uniform sampler2D openfl_Texture; - - varying vec2 vBlurCoords[7]; - - void main(void) { - - vec4 sum = vec4(0.0); - sum += texture2D(openfl_Texture, vBlurCoords[0]) * 0.00443; - sum += texture2D(openfl_Texture, vBlurCoords[1]) * 0.05399; - sum += texture2D(openfl_Texture, vBlurCoords[2]) * 0.24197; - sum += texture2D(openfl_Texture, vBlurCoords[3]) * 0.39894; - sum += texture2D(openfl_Texture, vBlurCoords[4]) * 0.24197; - sum += texture2D(openfl_Texture, vBlurCoords[5]) * 0.05399; - sum += texture2D(openfl_Texture, vBlurCoords[6]) * 0.00443; - - gl_FragColor = sum; - - }") - @:glVertexSource("attribute vec4 openfl_Position; - attribute vec2 openfl_TextureCoord; - - uniform mat4 openfl_Matrix; - - uniform vec2 uRadius; - varying vec2 vBlurCoords[7]; - uniform vec2 uTextureSize; - - void main(void) { - - gl_Position = openfl_Matrix * openfl_Position; - - vec2 r = uRadius / uTextureSize; - vBlurCoords[0] = openfl_TextureCoord - r; - vBlurCoords[1] = openfl_TextureCoord - r * 0.75; - vBlurCoords[2] = openfl_TextureCoord - r * 0.5; - vBlurCoords[3] = openfl_TextureCoord; - vBlurCoords[4] = openfl_TextureCoord + r * 0.5; - vBlurCoords[5] = openfl_TextureCoord + r * 0.75; - vBlurCoords[6] = openfl_TextureCoord + 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(); - } -} - -#if flash_box_blur // Flash-faithful fractional box blur, one axis per pass (Ruffle-style). Kernel: // full_size = blur (<=255); radius = (full_size-1)/2; m = ceil(radius)-1 interior // double-weighted bilinear pairs; alpha = frac edge weight (8-bit quantised); @@ -485,7 +387,6 @@ private class BoxBlurShader extends BitmapFilterShader super.__update(); } } -#end #else typedef BlurFilter = flash.filters.BlurFilter; #end diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index fb023e4846..ec056f5687 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -325,30 +325,8 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (blurPass < numBlurPasses) { var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; - #if flash_box_blur var horizontal = blurPass < __horizontalPasses; return GlowFilter.__setupBoxBlur(horizontal, horizontal ? blurX : blurY, color, alpha, strength); - #else - var shader = GlowFilter.__blurAlphaShader; - if (blurPass < __horizontalPasses) - { - var scale = Math.pow(0.5, blurPass >> 1) * 0.5; - shader.uRadius.value[0] = blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (blurPass - __horizontalPasses) >> 1) * 0.5; - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = blurY * scale; - } - shader.uColor.value[0] = ((color >> 16) & 0xFF) / 255; - shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; - shader.uColor.value[2] = (color & 0xFF) / 255; - shader.uColor.value[3] = alpha; - shader.uStrength.value[0] = strength; - return shader; - #end } if (__inner) { @@ -399,7 +377,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - #if flash_box_blur // Box blur applies `quality` passes; each pass widens the shadow's // support by ~half the blur, so the reach grows to ~quality*blur/2. If we // only reserve one blur radius the shadow is hard-clipped to a rectangle @@ -407,10 +384,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO var q = (__quality > 0) ? __quality : 1; var exX = Math.ceil(__blurX * 0.5 * q) + 4; var exY = Math.ceil(__blurY * 0.5 * q) + 4; - #else - var exX = Math.ceil(__blurX); - var exY = Math.ceil(__blurY); - #end __topExtension = Std.int((__offsetY < 0 ? -__offsetY : 0) + exY); __bottomExtension = Std.int((__offsetY > 0 ? __offsetY : 0) + exY); __leftExtension = Std.int((__offsetX < 0 ? -__offsetX : 0) + exX); @@ -420,14 +393,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __calculateNumShaderPasses():Void { - #if flash_box_blur var q = (__quality > 0) ? __quality : 1; __horizontalPasses = (__blurX <= 0) ? 0 : q; __verticalPasses = (__blurY <= 0) ? 0 : q; - #else - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; - #end __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index 3b1e5118db..c17ce621f7 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -68,12 +68,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:final class GlowFilter extends BitmapFilter { @:noCompletion private static var __invertAlphaShader = new InvertAlphaShader(); - @:noCompletion private static var __blurAlphaShader = new BlurAlphaShader(); - #if flash_box_blur // Flash-faithful fractional box blur of the alpha channel (Ruffle-style), - // colourised like BlurAlphaShader. Shared by GlowFilter + DropShadowFilter. + // colourised. Shared by GlowFilter + DropShadowFilter. @:noCompletion private static var __boxBlurAlphaShader = new BoxBlurAlphaShader(); - #end @:noCompletion private static var __combineShader = new CombineShader(); @:noCompletion private static var __innerCombineShader = new InnerCombineShader(); @:noCompletion private static var __combineKnockoutShader = new CombineKnockoutShader(); @@ -292,30 +289,8 @@ import lime._internal.graphics.ImageDataUtil; // TODO if (blurPass < numBlurPasses) { var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; - #if flash_box_blur var horizontal = blurPass < __horizontalPasses; return __setupBoxBlur(horizontal, horizontal ? blurX : blurY, color, alpha, strength); - #else - var shader = __blurAlphaShader; - if (blurPass < __horizontalPasses) - { - var scale = Math.pow(0.5, blurPass >> 1) * 0.5; - shader.uRadius.value[0] = blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (blurPass - __horizontalPasses) >> 1) * 0.5; - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = blurY * scale; - } - shader.uColor.value[0] = ((color >> 16) & 0xFF) / 255; - shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; - shader.uColor.value[2] = (color & 0xFF) / 255; - shader.uColor.value[3] = alpha; - shader.uStrength.value[0] = strength; - return shader; - #end } if (__inner) { @@ -356,16 +331,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __updateSize():Void { - #if flash_box_blur // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); // reserve the full spread so the glow isn't clipped at high quality. var q = (__quality > 0) ? __quality : 1; __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 0.5 * q) + 4 : 0); __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 0.5 * q) + 4 : 0); - #else - __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 1.5) : 0); - __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 1.5) : 0); - #end __rightExtension = __leftExtension; __bottomExtension = __topExtension; __calculateNumShaderPasses(); @@ -373,19 +343,13 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __calculateNumShaderPasses():Void { - #if flash_box_blur // one horizontal + one vertical box pass per quality iteration var q = (__quality > 0) ? __quality : 1; __horizontalPasses = (__blurX <= 0) ? 0 : q; __verticalPasses = (__blurY <= 0) ? 0 : q; - #else - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; - #end __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } - #if flash_box_blur // Configure the shared box-blur-alpha shader for one axis/pass (used by both // GlowFilter and DropShadowFilter). Same kernel math as BlurFilter's box blur. @:noCompletion private static function __setupBoxBlur(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader @@ -423,7 +387,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO s.uStrength.value[0] = strength; return s; } - #end // Get & Set Methods @:noCompletion private function get_alpha():Float @@ -570,80 +533,6 @@ private class InvertAlphaShader extends BitmapFilterShader } } -#if !openfl_debug -@:fileXml('tags="haxe,release"') -@:noDebug -#end -private class BlurAlphaShader extends BitmapFilterShader -{ - @:glFragmentSource(" - uniform sampler2D openfl_Texture; - uniform vec4 uColor; - uniform float uStrength; - varying vec2 vTexCoord; - varying vec2 vBlurCoords[6]; - - void main(void) - { - vec4 texel = texture2D(openfl_Texture, vTexCoord); - - vec3 contributions = vec3(0.00443, 0.05399, 0.24197); - vec3 top = vec3( - texture2D(openfl_Texture, vBlurCoords[0]).a, - texture2D(openfl_Texture, vBlurCoords[1]).a, - texture2D(openfl_Texture, vBlurCoords[2]).a - ); - vec3 bottom = vec3( - texture2D(openfl_Texture, vBlurCoords[3]).a, - texture2D(openfl_Texture, vBlurCoords[4]).a, - texture2D(openfl_Texture, vBlurCoords[5]).a - ); - - float a = texel.a * 0.39894; - a += dot(top, contributions.xyz); - a += dot(bottom, contributions.zyx); - - gl_FragColor = uColor * clamp(a * uStrength, 0.0, 1.0); - } - ") - @:glVertexSource(" - attribute vec4 openfl_Position; - attribute vec2 openfl_TextureCoord; - - uniform mat4 openfl_Matrix; - uniform vec2 openfl_TextureSize; - - uniform vec2 uRadius; - varying vec2 vTexCoord; - varying vec2 vBlurCoords[6]; - - void main(void) { - - gl_Position = openfl_Matrix * openfl_Position; - vTexCoord = openfl_TextureCoord; - - vec3 offset = vec3(0.5, 0.75, 1.0); - vec2 r = uRadius / openfl_TextureSize; - vBlurCoords[0] = openfl_TextureCoord - r * offset.z; - vBlurCoords[1] = openfl_TextureCoord - r * offset.y; - vBlurCoords[2] = openfl_TextureCoord - r * offset.x; - vBlurCoords[3] = openfl_TextureCoord + r * offset.x; - vBlurCoords[4] = openfl_TextureCoord + r * offset.y; - vBlurCoords[5] = openfl_TextureCoord + r * offset.z; - } - ") - public function new() - { - super(); - #if !macro - uRadius.value = [0, 0]; - uColor.value = [0, 0, 0, 0]; - uStrength.value = [1]; - #end - } -} - -#if flash_box_blur #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug @@ -714,7 +603,6 @@ private class BoxBlurAlphaShader extends BitmapFilterShader super.__update(); } } -#end #if !openfl_debug @:fileXml('tags="haxe,release"') diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index b600e92751..eabb80979f 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -101,24 +101,7 @@ import openfl.geom.Rectangle; if (pass < numBlurPasses) { var horizontal = pass < __horizontalPasses; - #if flash_box_blur return BlurFilter.__setupBoxBlur(horizontal, horizontal ? __blurX : __blurY); - #else - var shader = BlurFilter.__blurShader; - if (horizontal) - { - var scale = Math.pow(0.5, pass >> 1); - shader.uRadius.value[0] = __blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = __blurY * scale; - } - return shader; - #end } if (__rampDirty) __buildRamp(); @@ -191,25 +174,14 @@ import openfl.geom.Rectangle; // on every side); ceil the absolute value so negative angles don't lose a pixel var offsetX:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.cos(rad))) : 0; var offsetY:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.sin(rad))) : 0; - #if flash_box_blur - var qext = (__quality > 0) ? __quality : 1; - var exX = Math.ceil(__blurX * 0.5 * qext); - var exY = Math.ceil(__blurY * 0.5 * qext); - #else - var exX = Math.ceil(__blurX * 1.5); - var exY = Math.ceil(__blurY * 1.5); - #end + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q); + var exY = Math.ceil(__blurY * 0.5 * q); __leftExtension = __rightExtension = exX + offsetX; __topExtension = __bottomExtension = exY + offsetY; - #if flash_box_blur - var q = (__quality > 0) ? __quality : 1; __horizontalPasses = (__blurX <= 0) ? 0 : q; __verticalPasses = (__blurY <= 0) ? 0 : q; - #else - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; - #end __numShaderPasses = __horizontalPasses + __verticalPasses + 1; } diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index 7df9a34a23..663f28dea9 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -104,24 +104,7 @@ import openfl.geom.Rectangle; { // blur the object's alpha into a soft distance field (reuse BlurFilter) var horizontal = pass < __horizontalPasses; - #if flash_box_blur return BlurFilter.__setupBoxBlur(horizontal, horizontal ? __blurX : __blurY); - #else - var shader = BlurFilter.__blurShader; - if (horizontal) - { - var scale = Math.pow(0.5, pass >> 1); - shader.uRadius.value[0] = __blurX * scale; - shader.uRadius.value[1] = 0; - } - else - { - var scale = Math.pow(0.5, (pass - __horizontalPasses) >> 1); - shader.uRadius.value[0] = 0; - shader.uRadius.value[1] = __blurY * scale; - } - return shader; - #end } if (__rampDirty) __buildRamp(); @@ -183,29 +166,18 @@ import openfl.geom.Rectangle; { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - #if flash_box_blur // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); // reserve the full spread so the gradient glow isn't clipped at high quality. - var qext = (__quality > 0) ? __quality : 1; - var exX = Math.ceil(__blurX * 0.5 * qext) + 4; - var exY = Math.ceil(__blurY * 0.5 * qext) + 4; - #else - var exX = Math.ceil(__blurX * 1.5); - var exY = Math.ceil(__blurY * 1.5); - #end + var q = (__quality > 0) ? __quality : 1; + var exX = Math.ceil(__blurX * 0.5 * q) + 4; + var exY = Math.ceil(__blurY * 0.5 * q) + 4; __topExtension = (__offsetY < 0 ? -__offsetY : 0) + exY; __bottomExtension = (__offsetY > 0 ? __offsetY : 0) + exY; __leftExtension = (__offsetX < 0 ? -__offsetX : 0) + exX; __rightExtension = (__offsetX > 0 ? __offsetX : 0) + exX; - #if flash_box_blur - var q = (__quality > 0) ? __quality : 1; __horizontalPasses = (__blurX <= 0) ? 0 : q; __verticalPasses = (__blurY <= 0) ? 0 : q; - #else - __horizontalPasses = (__blurX <= 0) ? 0 : Math.round(__blurX * (__quality / 4)) + 1; - __verticalPasses = (__blurY <= 0) ? 0 : Math.round(__blurY * (__quality / 4)) + 1; - #end __numShaderPasses = __horizontalPasses + __verticalPasses + 1; } From af3b0bf2324d6bbfa4a62338b0ad34e2fb9bb8b8 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Tue, 1 Sep 2026 21:10:16 +0200 Subject: [PATCH 08/21] filters: name box-blur helpers/fields after the shader they configure Rename for clarity, no behaviour change: - BlurFilter.__setupBoxBlur -> __setupBlurShader (configures BoxBlurShader) - GlowFilter.__setupBoxBlur -> __setupBlurAlphaShader (configures BoxBlurAlphaShader) - GlowFilter.__boxBlurAlphaShader field -> __blurAlphaShader (symmetric with BlurFilter's existing __blurShader field) Callers updated accordingly: Blur/Bevel/GradientGlow/GradientBevel use the colour shader helper; Glow/DropShadow use the alpha shader helper. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BevelFilter.hx | 2 +- src/openfl/filters/BlurFilter.hx | 4 ++-- src/openfl/filters/DropShadowFilter.hx | 2 +- src/openfl/filters/GlowFilter.hx | 8 ++++---- src/openfl/filters/GradientBevelFilter.hx | 2 +- src/openfl/filters/GradientGlowFilter.hx | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index b786ffea1d..bc8e02ec0e 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -174,7 +174,7 @@ import lime._internal.graphics.ImageDataUtil; if (blurPass < numBlurPasses) { var horizontal = pass < __horizontalPasses; - return BlurFilter.__setupBoxBlur(horizontal, horizontal ? blurX : blurY); + return BlurFilter.__setupBlurShader(horizontal, horizontal ? blurX : blurY); } __bevelShader.sourceBitmap.input = sourceBitmapData; diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index 3a0b03c34f..7095fde077 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -200,7 +200,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO // passes alternate horizontal / vertical; each applies one full box blur // for its axis, iterated `quality` times (separable box passes commute) var horizontal = (pass % 2 == 0); - return __setupBoxBlur(horizontal, horizontal ? blurX : blurY); + return __setupBlurShader(horizontal, horizontal ? blurX : blurY); #else return __boxBlurShader; #end @@ -208,7 +208,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO // Configure the shared box-blur shader for one axis of one pass. Reused by // BevelFilter (which blurs the source before deriving highlight/shadow). - @:noCompletion private static function __setupBoxBlur(horizontal:Bool, v:Float):BitmapFilterShader + @:noCompletion private static function __setupBlurShader(horizontal:Bool, v:Float):BitmapFilterShader { var s = __boxBlurShader; var fullSize = v > 255 ? 255.0 : v; diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index ec056f5687..80e432bc5a 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -326,7 +326,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; var horizontal = blurPass < __horizontalPasses; - return GlowFilter.__setupBoxBlur(horizontal, horizontal ? blurX : blurY, color, alpha, strength); + return GlowFilter.__setupBlurAlphaShader(horizontal, horizontal ? blurX : blurY, color, alpha, strength); } if (__inner) { diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index c17ce621f7..bc46abe7df 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -70,7 +70,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private static var __invertAlphaShader = new InvertAlphaShader(); // Flash-faithful fractional box blur of the alpha channel (Ruffle-style), // colourised. Shared by GlowFilter + DropShadowFilter. - @:noCompletion private static var __boxBlurAlphaShader = new BoxBlurAlphaShader(); + @:noCompletion private static var __blurAlphaShader = new BoxBlurAlphaShader(); @:noCompletion private static var __combineShader = new CombineShader(); @:noCompletion private static var __innerCombineShader = new InnerCombineShader(); @:noCompletion private static var __combineKnockoutShader = new CombineKnockoutShader(); @@ -290,7 +290,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; var horizontal = blurPass < __horizontalPasses; - return __setupBoxBlur(horizontal, horizontal ? blurX : blurY, color, alpha, strength); + return __setupBlurAlphaShader(horizontal, horizontal ? blurX : blurY, color, alpha, strength); } if (__inner) { @@ -352,9 +352,9 @@ import lime._internal.graphics.ImageDataUtil; // TODO // Configure the shared box-blur-alpha shader for one axis/pass (used by both // GlowFilter and DropShadowFilter). Same kernel math as BlurFilter's box blur. - @:noCompletion private static function __setupBoxBlur(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader + @:noCompletion private static function __setupBlurAlphaShader(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader { - var s = __boxBlurAlphaShader; + var s = __blurAlphaShader; var fullSize = v > 255 ? 255.0 : v; s.uDir.value[0] = horizontal ? 1.0 : 0.0; s.uDir.value[1] = horizontal ? 0.0 : 1.0; diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index eabb80979f..37454dacac 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -101,7 +101,7 @@ import openfl.geom.Rectangle; if (pass < numBlurPasses) { var horizontal = pass < __horizontalPasses; - return BlurFilter.__setupBoxBlur(horizontal, horizontal ? __blurX : __blurY); + return BlurFilter.__setupBlurShader(horizontal, horizontal ? __blurX : __blurY); } if (__rampDirty) __buildRamp(); diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index 663f28dea9..43deb85761 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -104,7 +104,7 @@ import openfl.geom.Rectangle; { // blur the object's alpha into a soft distance field (reuse BlurFilter) var horizontal = pass < __horizontalPasses; - return BlurFilter.__setupBoxBlur(horizontal, horizontal ? __blurX : __blurY); + return BlurFilter.__setupBlurShader(horizontal, horizontal ? __blurX : __blurY); } if (__rampDirty) __buildRamp(); From 5d9bda876f1877fe3d981192fc0c0d2ba573bf24 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Tue, 1 Sep 2026 22:30:09 +0200 Subject: [PATCH 09/21] Box blur: per-texel fractional box (replaces the fused bilinear-pair form) Swap the implementation of BoxBlurShader and BoxBlurAlphaShader for a straight per-texel fractional box: a box of width uFullSize (= the blur amount) built as (2n+1) full-weight interior texels + one fractional-weight texel per edge, divided by the width, 8-bit rounded per pass. The class/field/method names are unchanged; all the box math now lives in the shader, so __setupBlurShader / __setupBlurAlphaShader only pass the axis, width, colour and strength. Weights are identical to the previous Ruffle-derived box, so output matches Flash/AIR to within 8-bit rounding (verified across the blur sweep and every blur-based filter: bevel, glow, gradient, stacks -- all unchanged). It also stays correct above blur 127, where the old shader's fixed 64-tap interior loop truncated the box. Trade-off: ~2x the texture fetches at high blur (no fused bilinear pairs) -- correctness over speed. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BlurFilter.hx | 87 ++++++++++++-------------------- src/openfl/filters/GlowFilter.hx | 83 ++++++++++++------------------ 2 files changed, 66 insertions(+), 104 deletions(-) diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index 7095fde077..6a112736a2 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -206,37 +206,15 @@ import lime._internal.graphics.ImageDataUtil; // TODO #end } - // Configure the shared box-blur shader for one axis of one pass. Reused by - // BevelFilter (which blurs the source before deriving highlight/shadow). + // Configure the box-blur shader for one axis of one pass. Reused by BevelFilter + // and the gradient filters. All the box math lives in the shader, so we only + // hand it the axis and the box width (= the blur amount). @:noCompletion private static function __setupBlurShader(horizontal:Bool, v:Float):BitmapFilterShader { var s = __boxBlurShader; - var fullSize = v > 255 ? 255.0 : v; s.uDir.value[0] = horizontal ? 1.0 : 0.0; s.uDir.value[1] = horizontal ? 0.0 : 1.0; - if (fullSize <= 1) - { - s.uFullSize.value[0] = 1.0; - s.uM.value[0] = 0.0; - s.uM2.value[0] = 0.0; - s.uFirstWeight.value[0] = 0.0; - s.uLastOffset.value[0] = 0.0; - s.uLastWeight.value[0] = 1.0; - } - else - { - var radius = (fullSize - 1) / 2; - var m = Math.ceil(radius) - 1; - if (m < 0) m = 0; - // fractional edge weight, 8-bit quantised to imitate Flash's fixed point - var frac = Math.floor((radius - m) * 255) / 255; - s.uFullSize.value[0] = fullSize; - s.uM.value[0] = m; - s.uM2.value[0] = m * 2; - s.uFirstWeight.value[0] = frac; - s.uLastOffset.value[0] = frac / (frac + 1); - s.uLastWeight.value[0] = frac + 1; - } + s.uFullSize.value[0] = v > 255 ? 255.0 : v; return s; } @@ -314,10 +292,12 @@ import lime._internal.graphics.ImageDataUtil; // TODO } } -// Flash-faithful fractional box blur, one axis per pass (Ruffle-style). Kernel: -// full_size = blur (<=255); radius = (full_size-1)/2; m = ceil(radius)-1 interior -// double-weighted bilinear pairs; alpha = frac edge weight (8-bit quantised); -// normalise by full_size; 8-bit round each pass to imitate Flash's fixed point. +// Flash-faithful fractional box blur, one axis per pass. Builds a box of width +// uFullSize (= the blur amount) straight from per-texel samples: (2n+1) full-weight +// interior texels + one fractional-weight texel per edge, divided by the width, and +// 8-bit rounded per pass to imitate Flash's fixed point. Weights match Ruffle's box +// filter, but computed directly rather than via its fused bilinear pairs -- which +// also keeps it correct above blur 127, where the paired form's 64-tap loop truncates. #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug @@ -334,33 +314,37 @@ private class BoxBlurShader extends BitmapFilterShader @:glFragmentSource("#pragma header uniform vec2 uTextureSize; - uniform vec2 uDir; - uniform float uFullSize; - uniform float uM; - uniform float uM2; - uniform float uFirstWeight; - uniform float uLastOffset; - uniform float uLastWeight; + uniform vec2 uDir; // blur axis: (1,0) horizontal, (0,1) vertical + uniform float uFullSize; // box width = the blur amount void main(void) { vec2 direction = uDir / uTextureSize; - vec2 base = openfl_TextureCoordv - direction * uM; - - vec4 total = texture2D(openfl_Texture, base - direction) * uFirstWeight; + float fullSize = min(uFullSize, 255.0); - vec4 center = vec4(0.0); - for (int i = 0; i < 64; i++) { - float fi = float(i) * 2.0 + 0.5; - if (fi >= uM2) break; - center += texture2D(openfl_Texture, base + direction * fi); + if (fullSize <= 1.0) { + gl_FragColor = texture2D(openfl_Texture, openfl_TextureCoordv); + return; } - total += center * 2.0; - total += texture2D(openfl_Texture, base + direction * (uM2 + uLastOffset)) * uLastWeight; + float halfW = fullSize * 0.5; + int n = int(floor(halfW - 0.5)); // full interior texels per side + float frac = halfW - (float(n) + 0.5); // fractional edge weight + frac = floor(frac * 255.0) / 255.0; // 8-bit weight (Flash fixed point) + + vec4 sum = texture2D(openfl_Texture, openfl_TextureCoordv); // centre + for (int i = 1; i <= 128; i++) { // full interior pairs + if (i > n) break; + vec2 off = float(i) * direction; + sum += texture2D(openfl_Texture, openfl_TextureCoordv + off); + sum += texture2D(openfl_Texture, openfl_TextureCoordv - off); + } + vec2 edge = float(n + 1) * direction; // fractional edges + sum += texture2D(openfl_Texture, openfl_TextureCoordv + edge) * frac; + sum += texture2D(openfl_Texture, openfl_TextureCoordv - edge) * frac; - vec4 result = total / uFullSize; - gl_FragColor = floor(result * 255.0) / 255.0; + vec4 result = sum / fullSize; + gl_FragColor = floor(result * 255.0) / 255.0; // 8-bit round each pass }") public function new() @@ -370,11 +354,6 @@ private class BoxBlurShader extends BitmapFilterShader #if !macro uDir.value = [1.0, 0.0]; uFullSize.value = [1.0]; - uM.value = [0.0]; - uM2.value = [0.0]; - uFirstWeight.value = [0.0]; - uLastOffset.value = [0.0]; - uLastWeight.value = [1.0]; #end } diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index bc46abe7df..ea5351c8d0 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -350,36 +350,15 @@ import lime._internal.graphics.ImageDataUtil; // TODO __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } - // Configure the shared box-blur-alpha shader for one axis/pass (used by both - // GlowFilter and DropShadowFilter). Same kernel math as BlurFilter's box blur. + // Configure the box-blur-alpha shader for one axis/pass (used by both GlowFilter + // and DropShadowFilter). All the box math lives in the shader; we hand it the + // axis, the box width (= the blur amount), the colour and the strength. @:noCompletion private static function __setupBlurAlphaShader(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader { var s = __blurAlphaShader; - var fullSize = v > 255 ? 255.0 : v; s.uDir.value[0] = horizontal ? 1.0 : 0.0; s.uDir.value[1] = horizontal ? 0.0 : 1.0; - if (fullSize <= 1) - { - s.uFullSize.value[0] = 1.0; - s.uM.value[0] = 0.0; - s.uM2.value[0] = 0.0; - s.uFirstWeight.value[0] = 0.0; - s.uLastOffset.value[0] = 0.0; - s.uLastWeight.value[0] = 1.0; - } - else - { - var radius = (fullSize - 1) / 2; - var m = Math.ceil(radius) - 1; - if (m < 0) m = 0; - var frac = Math.floor((radius - m) * 255) / 255; - s.uFullSize.value[0] = fullSize; - s.uM.value[0] = m; - s.uM2.value[0] = m * 2; - s.uFirstWeight.value[0] = frac; - s.uLastOffset.value[0] = frac / (frac + 1); - s.uLastWeight.value[0] = frac + 1; - } + s.uFullSize.value[0] = v > 255 ? 255.0 : v; s.uColor.value[0] = ((color >> 16) & 0xFF) / 255; s.uColor.value[1] = ((color >> 8) & 0xFF) / 255; s.uColor.value[2] = (color & 0xFF) / 255; @@ -537,6 +516,10 @@ private class InvertAlphaShader extends BitmapFilterShader @:fileXml('tags="haxe,release"') @:noDebug #end +// The alpha-channel twin of BlurFilter's BoxBlurShader: the same per-texel +// fractional box (2n+1 full interior texels + a fractional-weight edge texel per +// side), accumulated on .a, then colourised by uColor * clamp(a * strength) for +// glow / drop shadow. All the box math is in the shader. private class BoxBlurAlphaShader extends BitmapFilterShader { @:glFragmentSource("#pragma header @@ -544,32 +527,37 @@ private class BoxBlurAlphaShader extends BitmapFilterShader uniform vec4 uColor; uniform float uStrength; uniform vec2 uTextureSize; - uniform vec2 uDir; - uniform float uFullSize; - uniform float uM; - uniform float uM2; - uniform float uFirstWeight; - uniform float uLastOffset; - uniform float uLastWeight; + uniform vec2 uDir; // blur axis: (1,0) horizontal, (0,1) vertical + uniform float uFullSize; // box width = the blur amount void main(void) { vec2 direction = uDir / uTextureSize; - vec2 base = openfl_TextureCoordv - direction * uM; - - float total = texture2D(openfl_Texture, base - direction).a * uFirstWeight; - - float center = 0.0; - for (int i = 0; i < 64; i++) { - float fi = float(i) * 2.0 + 0.5; - if (fi >= uM2) break; - center += texture2D(openfl_Texture, base + direction * fi).a; + float fullSize = min(uFullSize, 255.0); + float a; + + if (fullSize <= 1.0) { + a = texture2D(openfl_Texture, openfl_TextureCoordv).a; + } else { + float halfW = fullSize * 0.5; + int n = int(floor(halfW - 0.5)); + float frac = halfW - (float(n) + 0.5); + frac = floor(frac * 255.0) / 255.0; + + float sum = texture2D(openfl_Texture, openfl_TextureCoordv).a; // centre + for (int i = 1; i <= 128; i++) { // full interior + if (i > n) break; + vec2 off = float(i) * direction; + sum += texture2D(openfl_Texture, openfl_TextureCoordv + off).a; + sum += texture2D(openfl_Texture, openfl_TextureCoordv - off).a; + } + vec2 edge = float(n + 1) * direction; // fractional edges + sum += texture2D(openfl_Texture, openfl_TextureCoordv + edge).a * frac; + sum += texture2D(openfl_Texture, openfl_TextureCoordv - edge).a * frac; + + a = sum / fullSize; } - total += center * 2.0; - - total += texture2D(openfl_Texture, base + direction * (uM2 + uLastOffset)).a * uLastWeight; - float a = total / uFullSize; gl_FragColor = uColor * clamp(a * uStrength, 0.0, 1.0); }") @:glVertexSource("#pragma header @@ -587,11 +575,6 @@ private class BoxBlurAlphaShader extends BitmapFilterShader uStrength.value = [1]; uDir.value = [1, 0]; uFullSize.value = [1]; - uM.value = [0]; - uM2.value = [0]; - uFirstWeight.value = [0]; - uLastOffset.value = [0]; - uLastWeight.value = [1]; #end } From edf5f77e5edcc66b6248bc88b4a76ecf7334fd8f Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Tue, 1 Sep 2026 22:49:27 +0200 Subject: [PATCH 10/21] BlurFilter: rename __boxBlurShader field to __blurShader Drops the redundant "box" from the field name, mirroring GlowFilter's __blurAlphaShader. Pure rename, no behaviour change. Co-Authored-By: Claude Opus 4.8 --- src/openfl/filters/BlurFilter.hx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index 6a112736a2..0c3afc8d31 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -71,7 +71,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { // Flash-faithful fractional box blur (Ruffle-style). Shared by the bevel and // gradient filters, which blur the source alpha before deriving their effect. - @:noCompletion private static var __boxBlurShader:BoxBlurShader = new BoxBlurShader(); + @:noCompletion private static var __blurShader:BoxBlurShader = new BoxBlurShader(); /** The amount of horizontal blur. Valid values are from 0 to 255(floating @@ -202,7 +202,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO var horizontal = (pass % 2 == 0); return __setupBlurShader(horizontal, horizontal ? blurX : blurY); #else - return __boxBlurShader; + return __blurShader; #end } @@ -211,7 +211,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO // hand it the axis and the box width (= the blur amount). @:noCompletion private static function __setupBlurShader(horizontal:Bool, v:Float):BitmapFilterShader { - var s = __boxBlurShader; + var s = __blurShader; s.uDir.value[0] = horizontal ? 1.0 : 0.0; s.uDir.value[1] = horizontal ? 0.0 : 1.0; s.uFullSize.value[0] = v > 255 ? 255.0 : v; From 6cee2600f78f875f96e85397065766e3b9512e82 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Tue, 1 Sep 2026 23:36:50 +0200 Subject: [PATCH 11/21] refactoring --- src/openfl/filters/BevelFilter.hx | 2 +- src/openfl/filters/BlurFilter.hx | 15 +--- src/openfl/filters/DropShadowFilter.hx | 10 ++- src/openfl/filters/GlowFilter.hx | 10 --- src/openfl/filters/GradientBevelFilter.hx | 97 ++++++++++++----------- src/openfl/filters/GradientGlowFilter.hx | 84 +++++++++++--------- 6 files changed, 108 insertions(+), 110 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index bc8e02ec0e..2db181b46f 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -341,7 +341,7 @@ import lime._internal.graphics.ImageDataUtil; if (value != __quality) __renderDirty = true; __quality = value; - __updateSize(); // extension depends on quality (box-blur reach) + __updateSize(); // depends on filter's quality settings return __quality = value; } diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index 0c3afc8d31..ca6b77f181 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -69,8 +69,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:access(openfl.geom.Rectangle) @:final class BlurFilter extends BitmapFilter { - // Flash-faithful fractional box blur (Ruffle-style). Shared by the bevel and - // gradient filters, which blur the source alpha before deriving their effect. @:noCompletion private static var __blurShader:BoxBlurShader = new BoxBlurShader(); /** @@ -197,8 +195,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader { #if !macro - // passes alternate horizontal / vertical; each applies one full box blur - // for its axis, iterated `quality` times (separable box passes commute) + // passes alternate horizontal / vertical, each applies one full box blur for its axis, iterated `quality` times var horizontal = (pass % 2 == 0); return __setupBlurShader(horizontal, horizontal ? blurX : blurY); #else @@ -206,9 +203,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO #end } - // Configure the box-blur shader for one axis of one pass. Reused by BevelFilter - // and the gradient filters. All the box math lives in the shader, so we only - // hand it the axis and the box width (= the blur amount). + // Configure the box-blur shader for one axis of one pass. @:noCompletion private static function __setupBlurShader(horizontal:Bool, v:Float):BitmapFilterShader { var s = __blurShader; @@ -292,12 +287,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO } } -// Flash-faithful fractional box blur, one axis per pass. Builds a box of width -// uFullSize (= the blur amount) straight from per-texel samples: (2n+1) full-weight -// interior texels + one fractional-weight texel per edge, divided by the width, and -// 8-bit rounded per pass to imitate Flash's fixed point. Weights match Ruffle's box -// filter, but computed directly rather than via its fused bilinear pairs -- which -// also keeps it correct above blur 127, where the paired form's 64-tap loop truncates. #if !openfl_debug @:fileXml('tags="haxe,release"') @:noDebug diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index 80e432bc5a..a7b8612894 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -377,13 +377,15 @@ import lime._internal.graphics.ImageDataUtil; // TODO { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - // Box blur applies `quality` passes; each pass widens the shadow's - // support by ~half the blur, so the reach grows to ~quality*blur/2. If we - // only reserve one blur radius the shadow is hard-clipped to a rectangle - // at high quality (Flash reserves room for the full spread). + + // Box blur applies `quality` passes (low, medium, high). + // each pass widens the shadow by approx. half the blur. + // so the reach grows to approx. `quality * blur / 2`. + // If we only reserve one blur radius the shadow is hard-clipped to a rectangle at high quality. var q = (__quality > 0) ? __quality : 1; var exX = Math.ceil(__blurX * 0.5 * q) + 4; var exY = Math.ceil(__blurY * 0.5 * q) + 4; + __topExtension = Std.int((__offsetY < 0 ? -__offsetY : 0) + exY); __bottomExtension = Std.int((__offsetY > 0 ? __offsetY : 0) + exY); __leftExtension = Std.int((__offsetX < 0 ? -__offsetX : 0) + exX); diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index ea5351c8d0..8c1adb172f 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -68,8 +68,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:final class GlowFilter extends BitmapFilter { @:noCompletion private static var __invertAlphaShader = new InvertAlphaShader(); - // Flash-faithful fractional box blur of the alpha channel (Ruffle-style), - // colourised. Shared by GlowFilter + DropShadowFilter. @:noCompletion private static var __blurAlphaShader = new BoxBlurAlphaShader(); @:noCompletion private static var __combineShader = new CombineShader(); @:noCompletion private static var __innerCombineShader = new InnerCombineShader(); @@ -332,7 +330,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function __updateSize():Void { // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); - // reserve the full spread so the glow isn't clipped at high quality. var q = (__quality > 0) ? __quality : 1; __leftExtension = (__blurX > 0 ? Math.ceil(__blurX * 0.5 * q) + 4 : 0); __topExtension = (__blurY > 0 ? Math.ceil(__blurY * 0.5 * q) + 4 : 0); @@ -350,9 +347,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO __numShaderPasses = __horizontalPasses + __verticalPasses + (__inner ? 2 : 1); } - // Configure the box-blur-alpha shader for one axis/pass (used by both GlowFilter - // and DropShadowFilter). All the box math lives in the shader; we hand it the - // axis, the box width (= the blur amount), the colour and the strength. @:noCompletion private static function __setupBlurAlphaShader(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader { var s = __blurAlphaShader; @@ -516,10 +510,6 @@ private class InvertAlphaShader extends BitmapFilterShader @:fileXml('tags="haxe,release"') @:noDebug #end -// The alpha-channel twin of BlurFilter's BoxBlurShader: the same per-texel -// fractional box (2n+1 full interior texels + a fractional-weight edge texel per -// side), accumulated on .a, then colourised by uColor * clamp(a * strength) for -// glow / drop shadow. All the box math is in the shader. private class BoxBlurAlphaShader extends BitmapFilterShader { @:glFragmentSource("#pragma header diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index 37454dacac..6bed2bf189 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -8,15 +8,8 @@ import openfl.geom.Point; import openfl.geom.Rectangle; /** - The GradientBevelFilter class lets you apply a gradient bevel effect to - display objects. The bevel's colours come from a gradient (defined by - `colors`/`alphas`/`ratios`) instead of separate highlight/shadow colours: - ratio 0 is one edge, 255 the other, and 128 is the base (usually - transparent), which appears where there is no bevel. - - Not present in stock OpenFL; implemented here for the non-flash targets by - sampling the blurred alpha at +/- the bevel offset to build a signed - distance field and indexing a 256-entry ramp built from the stops. + The GradientBevelFilter class lets you apply a gradient bevel effect to display objects. + The bevel's colours come from a gradient defined by `colors`/`alphas`/`ratios` instead of separate highlight/shadow colours. **/ #if !openfl_debug @:fileXml('tags="haxe,release"') @@ -90,7 +83,8 @@ import openfl.geom.Rectangle; @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // software path not implemented yet (GL shader path below is used on-screen) + // software path not implemented yet + // return the source unchanged so nothing crashes. return sourceBitmapData; } @@ -121,62 +115,74 @@ import openfl.geom.Rectangle; #end } - // Build the 256x1 straight-ARGB ramp from (colors, alphas, ratios). + // Build the 256-entry straight-ARGB gradient ramp (one texel per output index + // 0..255) from the (colors, alphas, ratios) stops. Each index is the colour and + // alpha linearly interpolated between the two stops it falls between. @:noCompletion private function __buildRamp():Void { if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); - var n = __colors.length; - var si = 0; - for (i in 0...256) + var stopCount = __colors.length; + var stop = 0; // the stop at or just before the current ramp index + + for (index in 0...256) { - while (si < n - 1 && __ratios[si + 1] < i) - si++; - var r0 = __ratios[si]; - var c0 = __colors[si]; - var a0 = __alphas[si]; - var rr:Float, gg:Float, bb:Float, aa:Float; - if (si >= n - 1 || i <= r0) + // advance to the stop pair whose ratio range contains `index` + while (stop < stopCount - 1 && __ratios[stop + 1] < index) + stop++; + + var colorLo = __colors[stop]; + var alphaLo = __alphas[stop]; + var r:Float, g:Float, b:Float, a:Float; + + if (stop >= stopCount - 1 || index <= __ratios[stop]) { - rr = (c0 >> 16) & 0xFF; - gg = (c0 >> 8) & 0xFF; - bb = c0 & 0xFF; - aa = a0 * 255; + // before the first stop, or past the last one: hold this stop's colour flat + r = (colorLo >> 16) & 0xFF; + g = (colorLo >> 8) & 0xFF; + b = colorLo & 0xFF; + a = alphaLo * 255; } else { - var r1 = __ratios[si + 1]; - var c1 = __colors[si + 1]; - var a1 = __alphas[si + 1]; - var f = (r1 > r0) ? (i - r0) / (r1 - r0) : 0.0; - rr = ((c0 >> 16) & 0xFF) + (((c1 >> 16) & 0xFF) - ((c0 >> 16) & 0xFF)) * f; - gg = ((c0 >> 8) & 0xFF) + (((c1 >> 8) & 0xFF) - ((c0 >> 8) & 0xFF)) * f; - bb = (c0 & 0xFF) + ((c1 & 0xFF) - (c0 & 0xFF)) * f; - aa = (a0 + (a1 - a0) * f) * 255; + var ratioLo = __ratios[stop]; + var ratioHi = __ratios[stop + 1]; + var colorHi = __colors[stop + 1]; + var alphaHi = __alphas[stop + 1]; + + // blend = how far `index` sits between the two stops (0 at the low + // stop, 1 at the high stop) + var blend = (ratioHi > ratioLo) ? (index - ratioLo) / (ratioHi - ratioLo) : 0.0; + + r = lerp((colorLo >> 16) & 0xFF, (colorHi >> 16) & 0xFF, blend); + g = lerp((colorLo >> 8) & 0xFF, (colorHi >> 8) & 0xFF, blend); + b = lerp(colorLo & 0xFF, colorHi & 0xFF, blend); + a = lerp(alphaLo, alphaHi, blend) * 255; } - var col = (Std.int(aa) << 24) | (Std.int(rr) << 16) | (Std.int(gg) << 8) | Std.int(bb); - __ramp.setPixel32(i, 0, col); + + var argb = (Std.int(a) << 24) | (Std.int(r) << 16) | (Std.int(g) << 8) | Std.int(b); + __ramp.setPixel32(index, 0, argb); } __rampDirty = false; } + @:noCompletion private static inline function lerp(a:Float, b:Float, t:Float):Float + { + return a + (b - a) * t; + } + @:noCompletion private function __updateSize():Void { - // The bevel field (bL-bR) is exactly zero beyond the box-blur support - // (quality*blur/2) offset by the transform (distance). Because the ramp's - // middle stop is usually opaque, flat regions map to the middle colour and - // the *visible* band edge sits at the texture boundary — so the extension - // must equal the true bevel extent (support + directional offset), with no - // safety margin, or the middle colour over-fills past where Flash stops. - // This mirrors BevelFilter's asymmetric extension (which matches Flash), - // minus the margin that filter can afford only because its band fades out. + // size calculation: box-blur support ( quality * blur/2 ) + transform offset(abs(distance*cos/sin)). + var rad = __angle * Math.PI / 180; - // magnitude of the transform offset per axis (band reaches support + |offset| - // on every side); ceil the absolute value so negative angles don't lose a pixel + // per-axis offset, ceil(abs) so negative angles don't drop a pixel var offsetX:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.cos(rad))) : 0; var offsetY:Int = (__type != INNER) ? Math.ceil(Math.abs(__distance * Math.sin(rad))) : 0; + var q = (__quality > 0) ? __quality : 1; var exX = Math.ceil(__blurX * 0.5 * q); var exY = Math.ceil(__blurY * 0.5 * q); + __leftExtension = __rightExtension = exX + offsetX; __topExtension = __bottomExtension = exY + offsetY; @@ -315,6 +321,7 @@ private class GradientBevelShader extends BitmapFilterShader else gl_FragColor = dest - dest * glow.a + glow; } }") + @:glVertexSource("attribute vec4 openfl_Position; attribute vec2 openfl_TextureCoord; uniform mat4 openfl_Matrix; diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index 43deb85761..4a4b08fffe 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -8,14 +8,8 @@ import openfl.geom.Point; import openfl.geom.Rectangle; /** - The GradientGlowFilter class lets you apply a gradient glow effect to - display objects. It is a glow whose colour is taken from a gradient (defined - by `colors`/`alphas`/`ratios`) instead of a single colour: `ratios` position - the colours along the glow — 0 is the outermost point, 255 the innermost. - - Not present in stock OpenFL; implemented here for the non-flash targets by - blurring the object's alpha into a distance field and indexing a 256-entry - ramp built from the stops. + The GradientGlowFilter class lets you apply a gradient glow effect to display objects. + The glow colours come from a gradient defined by `colors`/`alphas`/`ratios` instead of a single colour. **/ #if !openfl_debug @:fileXml('tags="haxe,release"') @@ -91,8 +85,8 @@ import openfl.geom.Rectangle; @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // software path not implemented yet (GL shader path below is the one used - // on-screen); return the source unchanged so nothing crashes. + // software path not implemented yet + // return the source unchanged so nothing crashes. return sourceBitmapData; } @@ -124,49 +118,66 @@ import openfl.geom.Rectangle; #end } - // Build the 256x1 straight-ARGB ramp from (colors, alphas, ratios). + // Build the 256-entry straight-ARGB gradient ramp (one texel per output index + // 0..255) from the (colors, alphas, ratios) stops. Each index is the colour and + // alpha linearly interpolated between the two stops it falls between. @:noCompletion private function __buildRamp():Void { if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); - var n = __colors.length; - var si = 0; - for (i in 0...256) + var stopCount = __colors.length; + var stop = 0; // the stop at or just before the current ramp index + + for (index in 0...256) { - while (si < n - 1 && __ratios[si + 1] < i) - si++; - var r0 = __ratios[si]; - var c0 = __colors[si]; - var a0 = __alphas[si]; - var rr:Float, gg:Float, bb:Float, aa:Float; - if (si >= n - 1 || i <= r0) + // advance to the stop pair whose ratio range contains `index` + while (stop < stopCount - 1 && __ratios[stop + 1] < index) + stop++; + + var colorLo = __colors[stop]; + var alphaLo = __alphas[stop]; + var r:Float, g:Float, b:Float, a:Float; + + if (stop >= stopCount - 1 || index <= __ratios[stop]) { - rr = (c0 >> 16) & 0xFF; - gg = (c0 >> 8) & 0xFF; - bb = c0 & 0xFF; - aa = a0 * 255; + // before the first stop, or past the last one: hold this stop's colour flat + r = (colorLo >> 16) & 0xFF; + g = (colorLo >> 8) & 0xFF; + b = colorLo & 0xFF; + a = alphaLo * 255; } else { - var r1 = __ratios[si + 1]; - var c1 = __colors[si + 1]; - var a1 = __alphas[si + 1]; - var f = (r1 > r0) ? (i - r0) / (r1 - r0) : 0.0; - rr = ((c0 >> 16) & 0xFF) + (((c1 >> 16) & 0xFF) - ((c0 >> 16) & 0xFF)) * f; - gg = ((c0 >> 8) & 0xFF) + (((c1 >> 8) & 0xFF) - ((c0 >> 8) & 0xFF)) * f; - bb = (c0 & 0xFF) + ((c1 & 0xFF) - (c0 & 0xFF)) * f; - aa = (a0 + (a1 - a0) * f) * 255; + var ratioLo = __ratios[stop]; + var ratioHi = __ratios[stop + 1]; + var colorHi = __colors[stop + 1]; + var alphaHi = __alphas[stop + 1]; + + // blend = how far `index` sits between the two stops (0 at the low + // stop, 1 at the high stop) + var blend = (ratioHi > ratioLo) ? (index - ratioLo) / (ratioHi - ratioLo) : 0.0; + + r = lerp((colorLo >> 16) & 0xFF, (colorHi >> 16) & 0xFF, blend); + g = lerp((colorLo >> 8) & 0xFF, (colorHi >> 8) & 0xFF, blend); + b = lerp(colorLo & 0xFF, colorHi & 0xFF, blend); + a = lerp(alphaLo, alphaHi, blend) * 255; } - var col = (Std.int(aa) << 24) | (Std.int(rr) << 16) | (Std.int(gg) << 8) | Std.int(bb); - __ramp.setPixel32(i, 0, col); + + var argb = (Std.int(a) << 24) | (Std.int(r) << 16) | (Std.int(g) << 8) | Std.int(b); + __ramp.setPixel32(index, 0, argb); } __rampDirty = false; } + @:noCompletion private static inline function lerp(a:Float, b:Float, t:Float):Float + { + return a + (b - a) * t; + } + @:noCompletion private function __updateSize():Void { __offsetX = Std.int(__distance * Math.cos(__angle * Math.PI / 180)); __offsetY = Std.int(__distance * Math.sin(__angle * Math.PI / 180)); - // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); + // Box blur reach grows to approx. `quality * blur / 2` per side (see DropShadowFilter); // reserve the full spread so the gradient glow isn't clipped at high quality. var q = (__quality > 0) ? __quality : 1; var exX = Math.ceil(__blurX * 0.5 * q) + 4; @@ -181,7 +192,6 @@ import openfl.geom.Rectangle; __numShaderPasses = __horizontalPasses + __verticalPasses + 1; } - // Getters / setters @:noCompletion private function get_distance():Float return __distance; @:noCompletion private function set_distance(v:Float):Float From 0c1d00fb8831919b2b287553f2821030c25e8dd9 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Wed, 2 Sep 2026 00:08:15 +0200 Subject: [PATCH 12/21] Bring the software filter path up to the GL path The CPU path (BitmapData.applyFilter, BitmapData.draw of a filtered object, software surfaces) diverged badly from both the GL path and Flash. Measured against AIR with a BitmapData.applyFilter harness, which reaches __applyFilter directly on every target and is Flash's own implementation on AIR. Four defects, all fixed: - BitmapData.applyFilter ignored its sourceBitmapData argument: it validated the parameter, then filtered `this`. The documented usage produced an empty bitmap. - Glow/DropShadow had no inner, no knockout, and ignored strength (lime's ImageDataUtil.gaussianBlur accepts strength/color but never uses them). - Bevel only blurred -- no highlight/shadow derivation or composite at all. - Both gradient filters were no-ops. Structural cause for the last three: the GL path hands the unfiltered object to its combine shader, which composites inner/knockout/full itself, while the software path relied on the caller drawing the object back on top -- which can only express "outer, non-knockout". Filters now composite inside __applyFilter and set the new BitmapFilter.__softwareComposite so the two software call sites skip that draw. The GL path never reads the flag and is unchanged. Shared CPU helpers on BitmapFilter mirror the shaders: __alphaField, __blurField (the same fractional box blur as BoxBlurShader) and __compositeEffect (the combine formulas). Each filter builds its effect layer as its shader does -- including inverting the alpha before blurring for inner glow/shadow, matching InvertAlphaShader. BlurFilter's software path is left alone: its StackBlur already matches Flash (0.23). Verified vs AIR across 12 cases: overall mean|diff| 14.95 -> 1.32, every case now below 1.0. GPU render unchanged (diff 0.0). Co-Authored-By: Claude Opus 4.8 --- src/openfl/display/BitmapData.hx | 6 +- src/openfl/display/DisplayObjectRenderer.hx | 2 +- src/openfl/filters/BevelFilter.hx | 50 +++- src/openfl/filters/BitmapFilter.hx | 253 ++++++++++++++++++++ src/openfl/filters/DropShadowFilter.hx | 52 +++- src/openfl/filters/GlowFilter.hx | 41 +++- src/openfl/filters/GradientBevelFilter.hx | 57 ++++- src/openfl/filters/GradientGlowFilter.hx | 52 +++- 8 files changed, 470 insertions(+), 43 deletions(-) diff --git a/src/openfl/display/BitmapData.hx b/src/openfl/display/BitmapData.hx index d292d9dbb9..8c5b5114fd 100644 --- a/src/openfl/display/BitmapData.hx +++ b/src/openfl/display/BitmapData.hx @@ -382,12 +382,12 @@ class BitmapData implements IBitmapDrawable if (filter.__preserveObject) { - bitmapData3.copyPixels(this, rect, destPoint); + bitmapData3.copyPixels(sourceBitmapData, sourceRect, destPoint); } - var lastBitmap = filter.__applyFilter(bitmapData2, this, sourceRect, destPoint); + var lastBitmap = filter.__applyFilter(bitmapData2, sourceBitmapData, sourceRect, destPoint); - if (filter.__preserveObject) + if (filter.__preserveObject && !filter.__softwareComposite) { lastBitmap.draw(bitmapData3, null, null); } diff --git a/src/openfl/display/DisplayObjectRenderer.hx b/src/openfl/display/DisplayObjectRenderer.hx index 1ddbaa9414..5045ee6a75 100644 --- a/src/openfl/display/DisplayObjectRenderer.hx +++ b/src/openfl/display/DisplayObjectRenderer.hx @@ -792,7 +792,7 @@ class DisplayObjectRenderer extends EventDispatcher lastBitmap = filter.__applyFilter(bitmap2, bitmap, bitmap.rect, destPoint); - if (filter.__preserveObject) + if (filter.__preserveObject && !filter.__softwareComposite) { lastBitmap.draw(bitmap3, null, displayObject.__objectTransform != null ? displayObject.__objectTransform.__colorTransform : null); diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 2db181b46f..45f0d6026b 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -144,6 +144,7 @@ import lime._internal.graphics.ImageDataUtil; __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; } @@ -155,15 +156,46 @@ import lime._internal.graphics.ImageDataUtil; @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - #if lime - var time = Timer.stamp(); - var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), destPoint.__toLimeVector2(), - __blurX, __blurY, __quality); - var elapsed = Timer.stamp() - time; - // trace("blurX: " + __blurX + " blurY: " + __blurY + " quality: " + __quality + " elapsed: " + elapsed * 1000 + "ms"); - if (finalImage == bitmapData.image) return bitmapData; - #end - return sourceBitmapData; + var width = bitmapData.width; + var height = bitmapData.height; + + // Same derivation as BevelShader: blur the source alpha, then compare the + // field either side of the light direction. The signed difference drives the + // highlight (light side) and the shadow (dark side). + var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + + var rad = __angle * Math.PI / 180; + var dx = Std.int(Math.round(__distance * Math.cos(rad))); + var dy = Std.int(Math.round(__distance * Math.sin(rad))); + + var hr = (((__highlightColor >> 16) & 0xFF) / 255.0) * __highlightAlpha; + var hg = (((__highlightColor >> 8) & 0xFF) / 255.0) * __highlightAlpha; + var hb = ((__highlightColor & 0xFF) / 255.0) * __highlightAlpha; + var sr = (((__shadowColor >> 16) & 0xFF) / 255.0) * __shadowAlpha; + var sg = (((__shadowColor >> 8) & 0xFF) / 255.0) * __shadowAlpha; + var sb = ((__shadowColor & 0xFF) / 255.0) * __shadowAlpha; + + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + var bL = BitmapFilter.__fieldAt(field, width, height, x + dx, y + dy); + var bR = BitmapFilter.__fieldAt(field, width, height, x - dx, y - dy); + var d = (bL - bR) * __strength; + var high = d > 1 ? 1.0 : (d < 0 ? 0.0 : d); + var shad = -d > 1 ? 1.0 : (-d < 0 ? 0.0 : -d); + + fxR.push(hr * high + sr * shad); + fxG.push(hg * high + sg * shad); + fxB.push(hb * high + sb * shad); + fxA.push(__highlightAlpha * high + __shadowAlpha * shad); + } + } + + var type:BitmapFilterType = (__type == "inner") ? INNER : ((__type == "outer") ? OUTER : FULL); + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, type, __knockout); } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader diff --git a/src/openfl/filters/BitmapFilter.hx b/src/openfl/filters/BitmapFilter.hx index 5e8937d7f0..d8504b6671 100644 --- a/src/openfl/filters/BitmapFilter.hx +++ b/src/openfl/filters/BitmapFilter.hx @@ -7,6 +7,7 @@ import openfl.display.DisplayObjectRenderer; import openfl.display.Shader; import openfl.geom.Point; import openfl.geom.Rectangle; +import openfl.Vector; /** The BitmapFilter class is the base class for all image filter effects. @@ -35,6 +36,18 @@ class BitmapFilter @:noCompletion private var __smooth:Bool; @:noCompletion private var __topExtension:Int; + /** + Whether `__applyFilter` composites the original object into its own result. + + The GPU path hands the unfiltered object to the combine shader (see + `__initShader`), which composites inner / knockout / full itself. The + software path instead relies on the caller drawing the object back on top + afterwards, which can only ever produce an "outer, non-knockout" result. + Filters whose `__applyFilter` does the whole composite set this so the + software callers skip that draw. The GPU path never reads it. + **/ + @:noCompletion private var __softwareComposite:Bool; + public function new() { __bottomExtension = 0; @@ -46,6 +59,7 @@ class BitmapFilter __shaderBlendMode = NORMAL; __topExtension = 0; __smooth = true; + __softwareComposite = false; } /** @@ -69,6 +83,245 @@ class BitmapFilter // return renderer.__defaultShader; return null; } + + // ------------------------------------------------------------------------ + // Software (CPU) helpers, shared by the effect filters. These mirror the GL + // shaders so both paths produce the same image: the same fractional box blur + // of the source alpha, and the same combine formulas. + // ------------------------------------------------------------------------ + + /** + The source's alpha channel as a 0..1 field laid out on the destination + grid, using the sourceRect/destPoint mapping `__applyFilter` is given. + Samples outside the source read 0, matching the transparent border the GL + path gets from its cache texture. + **/ + @:noCompletion private static function __alphaField(source:BitmapData, sourceRect:Rectangle, destPoint:Point, width:Int, height:Int):Array + { + var field = new Array(); + for (i in 0...width * height) + field.push(0.0); + + var pixels = source.getVector(sourceRect); + var sw = Std.int(sourceRect.width); + var sh = Std.int(sourceRect.height); + var ox = Std.int(destPoint.x); + var oy = Std.int(destPoint.y); + + for (y in 0...sh) + { + var dy = y + oy; + if (dy < 0 || dy >= height) continue; + for (x in 0...sw) + { + var dx = x + ox; + if (dx < 0 || dx >= width) continue; + field[dy * width + dx] = ((pixels[y * sw + x] >>> 24) & 0xFF) / 255.0; + } + } + return field; + } + + /** + Box-blur a 0..1 field in place, matching `BoxBlurShader`: `quality` + iterations of one horizontal then one vertical pass. + **/ + @:noCompletion private static function __blurField(field:Array, width:Int, height:Int, blurX:Float, blurY:Float, quality:Int):Array + { + var passes = (quality > 0) ? quality : 1; + var scratch = new Array(); + for (i in 0...field.length) + scratch.push(0.0); + + for (i in 0...passes) + { + __blurFieldAxis(field, scratch, width, height, blurX, true); + __blurFieldAxis(scratch, field, width, height, blurY, false); + } + return field; + } + + // One axis of the fractional box: the centre sample, `n` full-weight pairs, + // and a fractional-weight texel per edge, divided by the box width and + // rounded to 8 bits -- the same kernel BoxBlurShader uses. + @:noCompletion private static function __blurFieldAxis(src:Array, dest:Array, width:Int, height:Int, blur:Float, horizontal:Bool):Void + { + var fullSize = (blur > 255) ? 255.0 : blur; + + if (fullSize <= 1) + { + for (i in 0...src.length) + dest[i] = src[i]; + return; + } + + var half = fullSize * 0.5; + var n = Std.int(Math.floor(half - 0.5)); + if (n < 0) n = 0; + var frac = Math.floor((half - (n + 0.5)) * 255) / 255; + var edge = n + 1; + + for (y in 0...height) + { + for (x in 0...width) + { + var sum = __fieldAt(src, width, height, x, y); + + for (i in 1...(n + 1)) + { + if (horizontal) sum += __fieldAt(src, width, height, x + i, y) + __fieldAt(src, width, height, x - i, y); + else sum += __fieldAt(src, width, height, x, y + i) + __fieldAt(src, width, height, x, y - i); + } + + if (horizontal) sum += (__fieldAt(src, width, height, x + edge, y) + __fieldAt(src, width, height, x - edge, y)) * frac; + else sum += (__fieldAt(src, width, height, x, y + edge) + __fieldAt(src, width, height, x, y - edge)) * frac; + + dest[y * width + x] = Math.floor((sum / fullSize) * 255) / 255; + } + } + } + + @:noCompletion private static inline function __fieldAt(field:Array, width:Int, height:Int, x:Int, y:Int):Float + { + return (x < 0 || x >= width || y < 0 || y >= height) ? 0.0 : field[y * width + x]; + } + + /** + Combine an effect layer with the source and write the result into `dest`, + matching the GL combine shaders. The effect is supplied as premultiplied + 0..1 channels; `type` is INNER / OUTER / FULL. + + outer: src + fx * (1 - src.a) + inner: rgb = src.rgb * (1 - fx.a) + fx.rgb * src.a, a = src.a + full: src * (1 - fx.a) + fx + knockout: the src term is dropped (outer keeps `fx * (1 - src.a)`, + inner keeps `fx * src.a`, full keeps `fx`) + **/ + @:noCompletion private static function __compositeEffect(dest:BitmapData, source:BitmapData, sourceRect:Rectangle, destPoint:Point, fxR:Array, + fxG:Array, fxB:Array, fxA:Array, type:BitmapFilterType, knockout:Bool):BitmapData + { + var width = dest.width; + var height = dest.height; + + // source pixels on the destination grid, premultiplied + var srcR = new Array(), srcG = new Array(), srcB = new Array(), srcA = new Array(); + for (i in 0...width * height) + { + srcR.push(0.0); + srcG.push(0.0); + srcB.push(0.0); + srcA.push(0.0); + } + + var pixels = source.getVector(sourceRect); + var sw = Std.int(sourceRect.width); + var sh = Std.int(sourceRect.height); + var ox = Std.int(destPoint.x); + var oy = Std.int(destPoint.y); + + for (y in 0...sh) + { + var dy = y + oy; + if (dy < 0 || dy >= height) continue; + for (x in 0...sw) + { + var dx = x + ox; + if (dx < 0 || dx >= width) continue; + var argb = pixels[y * sw + x]; + var a = ((argb >>> 24) & 0xFF) / 255.0; + var i = dy * width + dx; + srcA[i] = a; + srcR[i] = (((argb >> 16) & 0xFF) / 255.0) * a; + srcG[i] = (((argb >> 8) & 0xFF) / 255.0) * a; + srcB[i] = ((argb & 0xFF) / 255.0) * a; + } + } + + var out = new Vector(width * height, true); + + for (i in 0...width * height) + { + var sr = srcR[i], sg = srcG[i], sb = srcB[i], sa = srcA[i]; + var er = fxR[i], eg = fxG[i], eb = fxB[i], ea = fxA[i]; + var r:Float, g:Float, b:Float, a:Float; + + if (type == INNER) + { + var mr = er * sa, mg = eg * sa, mb = eb * sa, ma = ea * sa; + if (knockout) + { + r = mr; + g = mg; + b = mb; + a = ma; + } + else + { + r = sr * (1 - ea) + mr; + g = sg * (1 - ea) + mg; + b = sb * (1 - ea) + mb; + a = sa; + } + } + else if (type == FULL) + { + if (knockout) + { + r = er; + g = eg; + b = eb; + a = ea; + } + else + { + r = sr * (1 - ea) + er; + g = sg * (1 - ea) + eg; + b = sb * (1 - ea) + eb; + a = sa * (1 - ea) + ea; + } + } + else // OUTER + { + var k = 1 - sa; + if (knockout) + { + r = er * k; + g = eg * k; + b = eb * k; + a = ea * k; + } + else + { + r = sr + er * k; + g = sg + eg * k; + b = sb + eb * k; + a = sa + ea * k; + } + } + + out[i] = __toStraightARGB(r, g, b, a); + } + + dest.setVector(dest.rect, out); + return dest; + } + + // premultiplied 0..1 -> straight 0xAARRGGBB + @:noCompletion private static inline function __toStraightARGB(r:Float, g:Float, b:Float, a:Float):UInt + { + if (a <= 0) return 0; + if (a > 1) a = 1; + var ir = Std.int(__clamp01(r / a) * 255 + 0.5); + var ig = Std.int(__clamp01(g / a) * 255 + 0.5); + var ib = Std.int(__clamp01(b / a) * 255 + 0.5); + var ia = Std.int(a * 255 + 0.5); + return (ia << 24) | (ir << 16) | (ig << 8) | ib; + } + + @:noCompletion private static inline function __clamp01(v:Float):Float + { + return v < 0 ? 0 : (v > 1 ? 1 : v); + } } #else typedef BitmapFilter = flash.filters.BitmapFilter; diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index a7b8612894..9e6767a1d6 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -282,6 +282,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; } @@ -292,22 +293,47 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // TODO: Support knockout, inner - - #if lime - var r = (__color >> 16) & 0xFF; - var g = (__color >> 8) & 0xFF; - var b = __color & 0xFF; + var width = bitmapData.width; + var height = bitmapData.height; + + // blur the source alpha, then read it back shifted by the shadow offset -- + // the GL path does the same by sampling the glow at (coord - offset). + // An inner shadow blurs the *inverted* alpha (the GL path runs + // InvertAlphaShader as its first pass) so the shadow falls inside the edge. + var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + if (__inner) + { + for (i in 0...field.length) + field[i] = 1 - field[i]; + } + BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); - var point = new Point(destPoint.x + __offsetX, destPoint.y + __offsetY); + var cr = ((__color >> 16) & 0xFF) / 255.0; + var cg = ((__color >> 8) & 0xFF) / 255.0; + var cb = (__color & 0xFF) / 255.0; + var ox = Std.int(__offsetX); + var oy = Std.int(__offsetY); - var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), point.__toLimeVector2(), - __blurX, __blurY, __quality, __strength); - finalImage.colorTransform(finalImage.rect, new ColorTransform(0, 0, 0, __alpha, r, g, b, 0).__toLimeColorMatrix()); + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + var sx = x - ox; + var sy = y - oy; + var f = ((sx < 0 || sx >= width || sy < 0 || sy >= height) ? 0.0 : field[sy * width + sx]) * __strength; + if (f > 1) f = 1; + else if (f < 0) f = 0; + fxR.push(cr * f); + fxG.push(cg * f); + fxB.push(cb * f); + fxA.push(__alpha * f); + } + } - if (finalImage == bitmapData.image) return bitmapData; - #end - return sourceBitmapData; + // hideObject drops the object just like knockout does + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __inner ? INNER : OUTER, + __knockout || __hideObject); } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index 8c1adb172f..0c90ea24da 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -246,6 +246,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; } @@ -256,20 +257,38 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // TODO: Support knockout, inner + var width = bitmapData.width; + var height = bitmapData.height; + + // blur the source alpha into the glow's coverage field, then colourise it + // exactly as BoxBlurAlphaShader does: fx = color * clamp(field * strength). + // An inner glow blurs the *inverted* alpha (the GL path runs InvertAlphaShader + // as its first pass), so the glow grows inwards from the edge. + var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + if (__inner) + { + for (i in 0...field.length) + field[i] = 1 - field[i]; + } + BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); - #if lime - var r = (__color >> 16) & 0xFF; - var g = (__color >> 8) & 0xFF; - var b = __color & 0xFF; + var cr = ((__color >> 16) & 0xFF) / 255.0; + var cg = ((__color >> 8) & 0xFF) / 255.0; + var cb = (__color & 0xFF) / 255.0; - var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), destPoint.__toLimeVector2(), - __blurX, __blurY, __quality, __strength); - finalImage.colorTransform(finalImage.rect, new ColorTransform(0, 0, 0, __alpha, r, g, b, 0).__toLimeColorMatrix()); + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (i in 0...width * height) + { + var f = field[i] * __strength; + if (f > 1) f = 1; + else if (f < 0) f = 0; + fxR.push(cr * f); + fxG.push(cg * f); + fxB.push(cb * f); + fxA.push(__alpha * f); + } - if (finalImage == bitmapData.image) return bitmapData; - #end - return sourceBitmapData; + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __inner ? INNER : OUTER, __knockout); } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index 6bed2bf189..1dbda4a4ec 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -70,6 +70,7 @@ import openfl.geom.Rectangle; __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; __updateSize(); @@ -83,9 +84,59 @@ import openfl.geom.Rectangle; @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // software path not implemented yet - // return the source unchanged so nothing crashes. - return sourceBitmapData; + var width = bitmapData.width; + var height = bitmapData.height; + + var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + + if (__rampDirty) __buildRamp(); + var ramp = __rampChannels(); + + var rad = __angle * Math.PI / 180; + var dx = Std.int(Math.round(__distance * Math.cos(rad))); + var dy = Std.int(Math.round(__distance * Math.sin(rad))); + + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + // signed bevel distance -> ramp index, as GradientBevelShader does: + // -1 is one edge, 0 the (usually transparent) middle stop, +1 the other + var bL = BitmapFilter.__fieldAt(field, width, height, x + dx, y + dy); + var bR = BitmapFilter.__fieldAt(field, width, height, x - dx, y - dy); + var sd = (bL - bR) * __strength; + if (sd > 1) sd = 1; + else if (sd < -1) sd = -1; + + var i = Std.int((sd * 0.5 + 0.5) * 255 + 0.5) * 4; + fxR.push(ramp[i]); + fxG.push(ramp[i + 1]); + fxB.push(ramp[i + 2]); + fxA.push(ramp[i + 3]); + } + } + + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __type, __knockout); + } + + // The 256-entry ramp as flat premultiplied [r,g,b,a] floats, matching how the + // ramp BitmapData is premultiplied when uploaded as a texture on the GL path. + @:noCompletion private function __rampChannels():Array + { + var out = new Array(); + var pixels = __ramp.getVector(__ramp.rect); + for (i in 0...256) + { + var argb = pixels[i]; + var a = ((argb >>> 24) & 0xFF) / 255.0; + out.push((((argb >> 16) & 0xFF) / 255.0) * a); + out.push((((argb >> 8) & 0xFF) / 255.0) * a); + out.push(((argb & 0xFF) / 255.0) * a); + out.push(a); + } + return out; } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index 4a4b08fffe..96ba3d7a80 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -72,6 +72,7 @@ import openfl.geom.Rectangle; __needSecondBitmapData = true; __preserveObject = true; + __softwareComposite = true; __renderDirty = true; __updateSize(); @@ -85,9 +86,54 @@ import openfl.geom.Rectangle; @:noCompletion private override function __applyFilter(bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData { - // software path not implemented yet - // return the source unchanged so nothing crashes. - return sourceBitmapData; + var width = bitmapData.width; + var height = bitmapData.height; + + // blurred source alpha, read back shifted by distance/angle (as the GL path + // samples the field at coord - offset) + var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + + if (__rampDirty) __buildRamp(); + var ramp = __rampChannels(); + + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); + for (y in 0...height) + { + for (x in 0...width) + { + var f = BitmapFilter.__fieldAt(field, width, height, x - __offsetX, y - __offsetY) * __strength; + if (f > 1) f = 1; + else if (f < 0) f = 0; + + // index the ramp by the field, exactly as GradientGlowShader does + var i = Std.int(f * 255 + 0.5) * 4; + fxR.push(ramp[i]); + fxG.push(ramp[i + 1]); + fxB.push(ramp[i + 2]); + fxA.push(ramp[i + 3]); + } + } + + return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __type, __knockout); + } + + // The 256-entry ramp as flat premultiplied [r,g,b,a] floats, matching how the + // ramp BitmapData is premultiplied when uploaded as a texture on the GL path. + @:noCompletion private function __rampChannels():Array + { + var out = new Array(); + var pixels = __ramp.getVector(__ramp.rect); + for (i in 0...256) + { + var argb = pixels[i]; + var a = ((argb >>> 24) & 0xFF) / 255.0; + out.push((((argb >> 16) & 0xFF) / 255.0) * a); + out.push((((argb >> 8) & 0xFF) / 255.0) * a); + out.push(((argb & 0xFF) / 255.0) * a); + out.push(a); + } + return out; } @:noCompletion private override function __initShader(renderer:DisplayObjectRenderer, pass:Int, sourceBitmapData:BitmapData):Shader From 4fa70aac747d7b133d6df8b9c9e51f8ff49eb198 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Wed, 2 Sep 2026 00:37:35 +0200 Subject: [PATCH 13/21] Scale filter distances by the device pixel ratio On a HiDPI display every blur-based filter rendered 1/pixelRatio too small relative to its content -- 0.67x at 1.5x DPI, 0.5x on a 2x panel. Measured against AIR, the outward reach of glow/blur/bevel was a consistent 0.64-0.67x. A filtered object is cached into a bitmap that is correctly sized and drawn at pixelRatio, but the filter's distances were never scaled to match. The shaders work in texels (direction = uDir / uTextureSize, uFullSize = blurX), so a blur of 12 covered 12 device pixels = 8 logical pixels at 1.5x. This predates the box blur: develop's Gaussian had the same flaw (vec2 r = uRadius / uTextureSize), and no filter referenced pixelRatio at all. Add BitmapFilter.__renderScale (default 1), set by the renderer to pixelRatio before each filter is used, on both the GPU and software cache loops. Every filter now scales its blur radii and its offsets/transforms by it, in both __initShader and __applyFilter. BitmapData.applyFilter leaves it at 1 -- it works on the bitmap's own pixels -- so that path is unaffected. Also: BevelFilter.__updateTransform() was only called from the distance/angle setters, i.e. at construction when the render scale is still 1, so scaling it there never took effect; it is now also called from __initShader. Verified vs AIR on a twelve-case harness: GPU mean|diff| 2.06 -> 0.36 (now better than the software path), bevel band width exactly AIR's, software column unchanged at 0.51. Co-Authored-By: Claude Opus 4.8 --- src/openfl/display/DisplayObjectRenderer.hx | 7 ++++++ src/openfl/filters/BevelFilter.hx | 15 ++++++----- src/openfl/filters/BitmapFilter.hx | 15 +++++++++++ src/openfl/filters/BlurFilter.hx | 4 +-- src/openfl/filters/DropShadowFilter.hx | 28 ++++++++++----------- src/openfl/filters/GlowFilter.hx | 4 +-- src/openfl/filters/GradientBevelFilter.hx | 12 ++++----- src/openfl/filters/GradientGlowFilter.hx | 10 ++++---- 8 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/openfl/display/DisplayObjectRenderer.hx b/src/openfl/display/DisplayObjectRenderer.hx index 5045ee6a75..c1f91879f4 100644 --- a/src/openfl/display/DisplayObjectRenderer.hx +++ b/src/openfl/display/DisplayObjectRenderer.hx @@ -668,6 +668,10 @@ class DisplayObjectRenderer extends EventDispatcher for (filter in displayObject.__filters) { + // the cache bitmap is drawn at pixelRatio, so blur radii and + // offsets (authored in logical pixels) must scale to match + filter.__renderScale = pixelRatio; + if (filter.__preserveObject) { childRenderer.__setRenderTarget(bitmap3); @@ -785,6 +789,9 @@ class DisplayObjectRenderer extends EventDispatcher for (filter in displayObject.__filters) { + // as above: the cache bitmap is drawn at pixelRatio + filter.__renderScale = pixelRatio; + if (filter.__preserveObject) { bitmap3.copyPixels(bitmap, bitmap.rect, destPoint); diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 45f0d6026b..c22b80af68 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -163,11 +163,11 @@ import lime._internal.graphics.ImageDataUtil; // field either side of the light direction. The signed difference drives the // highlight (light side) and the shadow (dark side). var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); - BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); var rad = __angle * Math.PI / 180; - var dx = Std.int(Math.round(__distance * Math.cos(rad))); - var dy = Std.int(Math.round(__distance * Math.sin(rad))); + var dx = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); + var dy = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); var hr = (((__highlightColor >> 16) & 0xFF) / 255.0) * __highlightAlpha; var hg = (((__highlightColor >> 8) & 0xFF) / 255.0) * __highlightAlpha; @@ -206,9 +206,12 @@ import lime._internal.graphics.ImageDataUtil; if (blurPass < numBlurPasses) { var horizontal = pass < __horizontalPasses; - return BlurFilter.__setupBlurShader(horizontal, horizontal ? blurX : blurY); + return BlurFilter.__setupBlurShader(horizontal, (horizontal ? blurX : blurY) * __renderScale); } + // recompute here rather than only in the distance/angle setters: the + // transform is scaled by __renderScale, which is not known until render time + __updateTransform(); __bevelShader.sourceBitmap.input = sourceBitmapData; #end @@ -440,8 +443,8 @@ import lime._internal.graphics.ImageDataUtil; @:noCompletion private function __updateTransform():Void { var rad:Float = __angle * Math.PI / 180; - __bevelShader.uTransformX.value[0] = (__distance * Math.cos(rad)); - __bevelShader.uTransformY.value[0] = (__distance * Math.sin(rad)); + __bevelShader.uTransformX.value[0] = (__distance * Math.cos(rad)) * __renderScale; + __bevelShader.uTransformY.value[0] = (__distance * Math.sin(rad)) * __renderScale; } @:noCompletion private function __updateColors():Void diff --git a/src/openfl/filters/BitmapFilter.hx b/src/openfl/filters/BitmapFilter.hx index d8504b6671..b10813a8a1 100644 --- a/src/openfl/filters/BitmapFilter.hx +++ b/src/openfl/filters/BitmapFilter.hx @@ -48,8 +48,23 @@ class BitmapFilter **/ @:noCompletion private var __softwareComposite:Bool; + /** + The device-pixel scale the filter is being rendered at. + + A filtered object is cached into a bitmap sized and drawn at the renderer's + pixel ratio, so on a HiDPI display the object is (say) 1.5x larger in that + bitmap. Filter distances -- blur radii and offsets -- are authored in + *logical* pixels, so they must be scaled to match, otherwise the effect comes + out 1/pixelRatio too small relative to its content. + + The renderer sets this before using the filter. It stays 1 for + `BitmapData.applyFilter`, which works on the bitmap's own pixels. + **/ + @:noCompletion private var __renderScale:Float; + public function new() { + __renderScale = 1; __bottomExtension = 0; __leftExtension = 0; __needSecondBitmapData = true; diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index ca6b77f181..cf457c2a7e 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -184,7 +184,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO #if lime var time = Timer.stamp(); var finalImage = ImageDataUtil.gaussianBlur(bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle(), destPoint.__toLimeVector2(), - __blurX, __blurY, __quality); + __blurX * __renderScale, __blurY * __renderScale, __quality); var elapsed = Timer.stamp() - time; // trace("blurX: " + __blurX + " blurY: " + __blurY + " quality: " + __quality + " elapsed: " + elapsed * 1000 + "ms"); if (finalImage == bitmapData.image) return bitmapData; @@ -197,7 +197,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO #if !macro // passes alternate horizontal / vertical, each applies one full box blur for its axis, iterated `quality` times var horizontal = (pass % 2 == 0); - return __setupBlurShader(horizontal, horizontal ? blurX : blurY); + return __setupBlurShader(horizontal, (horizontal ? blurX : blurY) * __renderScale); #else return __blurShader; #end diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index 9e6767a1d6..f6428488f7 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -306,13 +306,13 @@ import lime._internal.graphics.ImageDataUtil; // TODO for (i in 0...field.length) field[i] = 1 - field[i]; } - BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); var cr = ((__color >> 16) & 0xFF) / 255.0; var cg = ((__color >> 8) & 0xFF) / 255.0; var cb = (__color & 0xFF) / 255.0; - var ox = Std.int(__offsetX); - var oy = Std.int(__offsetY); + var ox = Std.int(__offsetX * __renderScale); + var oy = Std.int(__offsetY * __renderScale); var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (y in 0...height) @@ -352,7 +352,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; var horizontal = blurPass < __horizontalPasses; - return GlowFilter.__setupBlurAlphaShader(horizontal, horizontal ? blurX : blurY, color, alpha, strength); + return GlowFilter.__setupBlurAlphaShader(horizontal, (horizontal ? blurX : blurY) * __renderScale, color, alpha, strength); } if (__inner) { @@ -360,14 +360,14 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var shader = GlowFilter.__innerCombineKnockoutShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } var shader = GlowFilter.__innerCombineShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } else @@ -376,22 +376,22 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var shader = GlowFilter.__combineKnockoutShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } else if (__hideObject) { var shader = __hideShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } var shader = GlowFilter.__combineShader; shader.sourceBitmap.input = sourceBitmapData; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; return shader; } #else diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index 0c90ea24da..a778e252f5 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -270,7 +270,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO for (i in 0...field.length) field[i] = 1 - field[i]; } - BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); var cr = ((__color >> 16) & 0xFF) / 255.0; var cg = ((__color >> 8) & 0xFF) / 255.0; @@ -307,7 +307,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var strength = blurPass == (numBlurPasses - 1) ? __strength : 1.0; var horizontal = blurPass < __horizontalPasses; - return __setupBlurAlphaShader(horizontal, horizontal ? blurX : blurY, color, alpha, strength); + return __setupBlurAlphaShader(horizontal, (horizontal ? blurX : blurY) * __renderScale, color, alpha, strength); } if (__inner) { diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index 1dbda4a4ec..afe5f0e5a9 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -88,14 +88,14 @@ import openfl.geom.Rectangle; var height = bitmapData.height; var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); - BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); if (__rampDirty) __buildRamp(); var ramp = __rampChannels(); var rad = __angle * Math.PI / 180; - var dx = Std.int(Math.round(__distance * Math.cos(rad))); - var dy = Std.int(Math.round(__distance * Math.sin(rad))); + var dx = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); + var dy = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (y in 0...height) @@ -146,7 +146,7 @@ import openfl.geom.Rectangle; if (pass < numBlurPasses) { var horizontal = pass < __horizontalPasses; - return BlurFilter.__setupBlurShader(horizontal, horizontal ? __blurX : __blurY); + return BlurFilter.__setupBlurShader(horizontal, (horizontal ? __blurX : __blurY) * __renderScale); } if (__rampDirty) __buildRamp(); @@ -155,8 +155,8 @@ import openfl.geom.Rectangle; var shader = __gradientShader; shader.sourceBitmap.input = sourceBitmapData; shader.gradientRamp.input = __ramp; - shader.uTransformX.value[0] = __distance * Math.cos(rad); - shader.uTransformY.value[0] = __distance * Math.sin(rad); + shader.uTransformX.value[0] = __distance * Math.cos(rad) * __renderScale; + shader.uTransformY.value[0] = __distance * Math.sin(rad) * __renderScale; shader.uStrength.value[0] = __strength; shader.uBevelType.value[0] = (__type == INNER) ? 0.0 : (__type == OUTER ? 1.0 : 2.0); shader.uKnockout.value[0] = __knockout; diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index 96ba3d7a80..3b07c52b0e 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -92,7 +92,7 @@ import openfl.geom.Rectangle; // blurred source alpha, read back shifted by distance/angle (as the GL path // samples the field at coord - offset) var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); - BitmapFilter.__blurField(field, width, height, __blurX, __blurY, __quality); + BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); if (__rampDirty) __buildRamp(); var ramp = __rampChannels(); @@ -102,7 +102,7 @@ import openfl.geom.Rectangle; { for (x in 0...width) { - var f = BitmapFilter.__fieldAt(field, width, height, x - __offsetX, y - __offsetY) * __strength; + var f = BitmapFilter.__fieldAt(field, width, height, x - Std.int(__offsetX * __renderScale), y - Std.int(__offsetY * __renderScale)) * __strength; if (f > 1) f = 1; else if (f < 0) f = 0; @@ -144,7 +144,7 @@ import openfl.geom.Rectangle; { // blur the object's alpha into a soft distance field (reuse BlurFilter) var horizontal = pass < __horizontalPasses; - return BlurFilter.__setupBlurShader(horizontal, horizontal ? __blurX : __blurY); + return BlurFilter.__setupBlurShader(horizontal, (horizontal ? __blurX : __blurY) * __renderScale); } if (__rampDirty) __buildRamp(); @@ -152,8 +152,8 @@ import openfl.geom.Rectangle; var shader = __gradientShader; shader.sourceBitmap.input = sourceBitmapData; shader.gradientRamp.input = __ramp; - shader.offset.value[0] = __offsetX; - shader.offset.value[1] = __offsetY; + shader.offset.value[0] = __offsetX * __renderScale; + shader.offset.value[1] = __offsetY * __renderScale; shader.uStrength.value[0] = __strength; shader.uInner.value[0] = (__type == INNER) ? 1.0 : 0.0; shader.uFull.value[0] = (__type == FULL) ? 1.0 : 0.0; From c8920810284ff77baa9bcba0d6cda21b136de668 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Wed, 2 Sep 2026 01:42:53 +0200 Subject: [PATCH 14/21] Snap the filter cache bitmap to a whole device pixel __updateCacheBitmap rounded the cache origin (offsetX/offsetY) to a whole LOGICAL pixel. At a fractional pixelRatio (1.5x, 1.25x) that is a fractional DEVICE pixel, so the entire cache bitmap was composited to screen at a half-pixel phase and bilinearly smeared. Soft effects hide it; a thin high-contrast band -- a bevel edge -- shows it as a visible displacement. Found via gradientBevel: its extension of 11 logical px put the content at 16.5 device px, and a sub-pixel shift search showed the band displaced by ~0.75px diagonally, recoverable to 0.19 by translation alone. BevelFilter escaped only because its extension is always even (+4), and even x 1.5 is always an integer. Round the origin in device space instead (ceil/floor of rect.x * pixelRatio, then divide back). At pixelRatio 1 this is byte-identical to before; at 1.5x it aligns both the content-into-cache and cache-onto-screen placements. It never under-reserves: floor on the negative left offset only ever adds room. Verified vs AIR on the twelve-case three-way harness: gradientBevel 1.23 -> 0.15 with zero residual shift, every other case unchanged to 2 dp, software column unchanged, GPU average 0.36 -> 0.27. Its opaque-middle area now matches Flash to within 2 px (24294 vs 24292), which also retires the earlier "extent 1px short" observation -- that was the same smear. Co-Authored-By: Claude Fable 5.1 --- src/openfl/display/DisplayObjectRenderer.hx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/openfl/display/DisplayObjectRenderer.hx b/src/openfl/display/DisplayObjectRenderer.hx index c1f91879f4..ff93c8389a 100644 --- a/src/openfl/display/DisplayObjectRenderer.hx +++ b/src/openfl/display/DisplayObjectRenderer.hx @@ -387,8 +387,15 @@ class DisplayObjectRenderer extends EventDispatcher filterWidth = rect.width > 0 ? Math.ceil((rect.width + 1) * pixelRatio) : 0; filterHeight = rect.height > 0 ? Math.ceil((rect.height + 1) * pixelRatio) : 0; - offsetX = rect.x > 0 ? Math.ceil(rect.x) : Math.floor(rect.x); - offsetY = rect.y > 0 ? Math.ceil(rect.y) : Math.floor(rect.y); + // Snap the cache origin to a whole DEVICE pixel, not a whole logical + // pixel. At a fractional pixelRatio (1.5x, 1.25x) an integer logical + // offset is a fractional device offset, and the whole cache bitmap is + // then composited at a half-pixel phase and bilinearly smeared. Soft + // effects hide it; a thin high-contrast band (a bevel edge) shows it as + // a visible displacement. Rounding in device space keeps both the + // content-into-cache and cache-onto-screen placements pixel-aligned. + offsetX = (rect.x > 0 ? Math.ceil(rect.x * pixelRatio) : Math.floor(rect.x * pixelRatio)) / pixelRatio; + offsetY = (rect.y > 0 ? Math.ceil(rect.y * pixelRatio) : Math.floor(rect.y * pixelRatio)) / pixelRatio; if (displayObject.__cacheBitmapData != null) { From 04045372eb283d17a6b503e0cba47e2bae9027f0 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Wed, 2 Sep 2026 21:42:47 +0200 Subject: [PATCH 15/21] refactoring --- src/openfl/display/DisplayObjectRenderer.hx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/openfl/display/DisplayObjectRenderer.hx b/src/openfl/display/DisplayObjectRenderer.hx index ff93c8389a..5cfede71fc 100644 --- a/src/openfl/display/DisplayObjectRenderer.hx +++ b/src/openfl/display/DisplayObjectRenderer.hx @@ -387,13 +387,8 @@ class DisplayObjectRenderer extends EventDispatcher filterWidth = rect.width > 0 ? Math.ceil((rect.width + 1) * pixelRatio) : 0; filterHeight = rect.height > 0 ? Math.ceil((rect.height + 1) * pixelRatio) : 0; - // Snap the cache origin to a whole DEVICE pixel, not a whole logical - // pixel. At a fractional pixelRatio (1.5x, 1.25x) an integer logical - // offset is a fractional device offset, and the whole cache bitmap is - // then composited at a half-pixel phase and bilinearly smeared. Soft - // effects hide it; a thin high-contrast band (a bevel edge) shows it as - // a visible displacement. Rounding in device space keeps both the - // content-into-cache and cache-onto-screen placements pixel-aligned. + + // PixelRatio is adjusting for HDPI screns with fractional scaling. (1.5x, 1.25x etc) offsetX = (rect.x > 0 ? Math.ceil(rect.x * pixelRatio) : Math.floor(rect.x * pixelRatio)) / pixelRatio; offsetY = (rect.y > 0 ? Math.ceil(rect.y * pixelRatio) : Math.floor(rect.y * pixelRatio)) / pixelRatio; From 6ff7aca0c549386dfd9c1eef450c6a0a7b9bd322 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Thu, 3 Sep 2026 19:39:48 +0200 Subject: [PATCH 16/21] refactoring --- src/openfl/display/DisplayObjectRenderer.hx | 5 ++--- src/openfl/filters/BevelFilter.hx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/openfl/display/DisplayObjectRenderer.hx b/src/openfl/display/DisplayObjectRenderer.hx index 5cfede71fc..8d9682bace 100644 --- a/src/openfl/display/DisplayObjectRenderer.hx +++ b/src/openfl/display/DisplayObjectRenderer.hx @@ -670,8 +670,8 @@ class DisplayObjectRenderer extends EventDispatcher for (filter in displayObject.__filters) { - // the cache bitmap is drawn at pixelRatio, so blur radii and - // offsets (authored in logical pixels) must scale to match + // the cache bitmap is drawn at pixelRatio, so blur radius and + // offsets (in logical pixels) must scale to match filter.__renderScale = pixelRatio; if (filter.__preserveObject) @@ -791,7 +791,6 @@ class DisplayObjectRenderer extends EventDispatcher for (filter in displayObject.__filters) { - // as above: the cache bitmap is drawn at pixelRatio filter.__renderScale = pixelRatio; if (filter.__preserveObject) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index c22b80af68..bc4a9c8d47 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -209,7 +209,6 @@ import lime._internal.graphics.ImageDataUtil; return BlurFilter.__setupBlurShader(horizontal, (horizontal ? blurX : blurY) * __renderScale); } - // recompute here rather than only in the distance/angle setters: the // transform is scaled by __renderScale, which is not known until render time __updateTransform(); __bevelShader.sourceBitmap.input = sourceBitmapData; From 8f6cda6d95e89c429a25830c36dfa38e809699ac Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Thu, 3 Sep 2026 19:55:09 +0200 Subject: [PATCH 17/21] Rename the CPU filter helpers from "field" to "mask" __alphaField -> __alphaMask, __blurField -> __blurMask, __blurFieldAxis -> __blurMaskAxis, __fieldAt -> __maskAt, and the local variables to match. "Field" was scalar-field jargon that needed explaining; "mask" is what most readers expect a blurred single-channel alpha array to be called. The helpers header now says what the array represents: the shape's alpha, one Float per pixel, which blurred becomes the soft coverage map every effect is derived from. The shader comments that say "distance field" are left as-is: there the word names the signed-distance concept the bevel maths actually uses. Pure rename, no behaviour change; builds clean. Co-Authored-By: Claude Fable 5.1 --- src/openfl/filters/BevelFilter.hx | 10 ++--- src/openfl/filters/BitmapFilter.hx | 51 ++++++++++++----------- src/openfl/filters/DropShadowFilter.hx | 10 ++--- src/openfl/filters/GlowFilter.hx | 14 +++---- src/openfl/filters/GradientBevelFilter.hx | 8 ++-- src/openfl/filters/GradientGlowFilter.hx | 14 +++---- 6 files changed, 55 insertions(+), 52 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index bc4a9c8d47..15f369bca4 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -160,10 +160,10 @@ import lime._internal.graphics.ImageDataUtil; var height = bitmapData.height; // Same derivation as BevelShader: blur the source alpha, then compare the - // field either side of the light direction. The signed difference drives the + // mask either side of the light direction. The signed difference drives the // highlight (light side) and the shadow (dark side). - var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); - BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); var rad = __angle * Math.PI / 180; var dx = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); @@ -181,8 +181,8 @@ import lime._internal.graphics.ImageDataUtil; { for (x in 0...width) { - var bL = BitmapFilter.__fieldAt(field, width, height, x + dx, y + dy); - var bR = BitmapFilter.__fieldAt(field, width, height, x - dx, y - dy); + var bL = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); + var bR = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); var d = (bL - bR) * __strength; var high = d > 1 ? 1.0 : (d < 0 ? 0.0 : d); var shad = -d > 1 ? 1.0 : (-d < 0 ? 0.0 : -d); diff --git a/src/openfl/filters/BitmapFilter.hx b/src/openfl/filters/BitmapFilter.hx index b10813a8a1..75d8cbd6d0 100644 --- a/src/openfl/filters/BitmapFilter.hx +++ b/src/openfl/filters/BitmapFilter.hx @@ -53,7 +53,7 @@ class BitmapFilter A filtered object is cached into a bitmap sized and drawn at the renderer's pixel ratio, so on a HiDPI display the object is (say) 1.5x larger in that - bitmap. Filter distances -- blur radii and offsets -- are authored in + bitmap. Filter distances, blur radius, and offsets are authored in *logical* pixels, so they must be scaled to match, otherwise the effect comes out 1/pixelRatio too small relative to its content. @@ -103,19 +103,25 @@ class BitmapFilter // Software (CPU) helpers, shared by the effect filters. These mirror the GL // shaders so both paths produce the same image: the same fractional box blur // of the source alpha, and the same combine formulas. + // + // "mask" throughout = the shape's alpha as one Float (0..1) per pixel of the + // destination grid. Blurred, it becomes a soft coverage map -- 1 inside the + // shape, fading to 0 outside -- and that is the geometry every effect is + // derived from: glow/shadow read it, bevel takes its directional difference, + // the gradient filters index a colour ramp by it. // ------------------------------------------------------------------------ /** - The source's alpha channel as a 0..1 field laid out on the destination + The source's alpha channel as a 0..1 mask laid out on the destination grid, using the sourceRect/destPoint mapping `__applyFilter` is given. Samples outside the source read 0, matching the transparent border the GL path gets from its cache texture. **/ - @:noCompletion private static function __alphaField(source:BitmapData, sourceRect:Rectangle, destPoint:Point, width:Int, height:Int):Array + @:noCompletion private static function __alphaMask(source:BitmapData, sourceRect:Rectangle, destPoint:Point, width:Int, height:Int):Array { - var field = new Array(); + var mask = new Array(); for (i in 0...width * height) - field.push(0.0); + mask.push(0.0); var pixels = source.getVector(sourceRect); var sw = Std.int(sourceRect.width); @@ -131,35 +137,32 @@ class BitmapFilter { var dx = x + ox; if (dx < 0 || dx >= width) continue; - field[dy * width + dx] = ((pixels[y * sw + x] >>> 24) & 0xFF) / 255.0; + mask[dy * width + dx] = ((pixels[y * sw + x] >>> 24) & 0xFF) / 255.0; } } - return field; + return mask; } /** - Box-blur a 0..1 field in place, matching `BoxBlurShader`: `quality` + Box-blur a 0..1 mask in place, matching `BoxBlurShader`: `quality` iterations of one horizontal then one vertical pass. **/ - @:noCompletion private static function __blurField(field:Array, width:Int, height:Int, blurX:Float, blurY:Float, quality:Int):Array + @:noCompletion private static function __blurMask(mask:Array, width:Int, height:Int, blurX:Float, blurY:Float, quality:Int):Array { var passes = (quality > 0) ? quality : 1; var scratch = new Array(); - for (i in 0...field.length) + for (i in 0...mask.length) scratch.push(0.0); for (i in 0...passes) { - __blurFieldAxis(field, scratch, width, height, blurX, true); - __blurFieldAxis(scratch, field, width, height, blurY, false); + __blurMaskAxis(mask, scratch, width, height, blurX, true); + __blurMaskAxis(scratch, mask, width, height, blurY, false); } - return field; + return mask; } - // One axis of the fractional box: the centre sample, `n` full-weight pairs, - // and a fractional-weight texel per edge, divided by the box width and - // rounded to 8 bits -- the same kernel BoxBlurShader uses. - @:noCompletion private static function __blurFieldAxis(src:Array, dest:Array, width:Int, height:Int, blur:Float, horizontal:Bool):Void + @:noCompletion private static function __blurMaskAxis(src:Array, dest:Array, width:Int, height:Int, blur:Float, horizontal:Bool):Void { var fullSize = (blur > 255) ? 255.0 : blur; @@ -180,25 +183,25 @@ class BitmapFilter { for (x in 0...width) { - var sum = __fieldAt(src, width, height, x, y); + var sum = __maskAt(src, width, height, x, y); for (i in 1...(n + 1)) { - if (horizontal) sum += __fieldAt(src, width, height, x + i, y) + __fieldAt(src, width, height, x - i, y); - else sum += __fieldAt(src, width, height, x, y + i) + __fieldAt(src, width, height, x, y - i); + if (horizontal) sum += __maskAt(src, width, height, x + i, y) + __maskAt(src, width, height, x - i, y); + else sum += __maskAt(src, width, height, x, y + i) + __maskAt(src, width, height, x, y - i); } - if (horizontal) sum += (__fieldAt(src, width, height, x + edge, y) + __fieldAt(src, width, height, x - edge, y)) * frac; - else sum += (__fieldAt(src, width, height, x, y + edge) + __fieldAt(src, width, height, x, y - edge)) * frac; + if (horizontal) sum += (__maskAt(src, width, height, x + edge, y) + __maskAt(src, width, height, x - edge, y)) * frac; + else sum += (__maskAt(src, width, height, x, y + edge) + __maskAt(src, width, height, x, y - edge)) * frac; dest[y * width + x] = Math.floor((sum / fullSize) * 255) / 255; } } } - @:noCompletion private static inline function __fieldAt(field:Array, width:Int, height:Int, x:Int, y:Int):Float + @:noCompletion private static inline function __maskAt(mask:Array, width:Int, height:Int, x:Int, y:Int):Float { - return (x < 0 || x >= width || y < 0 || y >= height) ? 0.0 : field[y * width + x]; + return (x < 0 || x >= width || y < 0 || y >= height) ? 0.0 : mask[y * width + x]; } /** diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index f6428488f7..54390a8e4c 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -300,13 +300,13 @@ import lime._internal.graphics.ImageDataUtil; // TODO // the GL path does the same by sampling the glow at (coord - offset). // An inner shadow blurs the *inverted* alpha (the GL path runs // InvertAlphaShader as its first pass) so the shadow falls inside the edge. - var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); if (__inner) { - for (i in 0...field.length) - field[i] = 1 - field[i]; + for (i in 0...mask.length) + mask[i] = 1 - mask[i]; } - BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); var cr = ((__color >> 16) & 0xFF) / 255.0; var cg = ((__color >> 8) & 0xFF) / 255.0; @@ -321,7 +321,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { var sx = x - ox; var sy = y - oy; - var f = ((sx < 0 || sx >= width || sy < 0 || sy >= height) ? 0.0 : field[sy * width + sx]) * __strength; + var f = ((sx < 0 || sx >= width || sy < 0 || sy >= height) ? 0.0 : mask[sy * width + sx]) * __strength; if (f > 1) f = 1; else if (f < 0) f = 0; fxR.push(cr * f); diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index a778e252f5..4804928182 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -260,17 +260,17 @@ import lime._internal.graphics.ImageDataUtil; // TODO var width = bitmapData.width; var height = bitmapData.height; - // blur the source alpha into the glow's coverage field, then colourise it - // exactly as BoxBlurAlphaShader does: fx = color * clamp(field * strength). + // blur the source alpha into the glow's coverage mask, then colourise it + // exactly as BoxBlurAlphaShader does: fx = color * clamp(mask * strength). // An inner glow blurs the *inverted* alpha (the GL path runs InvertAlphaShader // as its first pass), so the glow grows inwards from the edge. - var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); if (__inner) { - for (i in 0...field.length) - field[i] = 1 - field[i]; + for (i in 0...mask.length) + mask[i] = 1 - mask[i]; } - BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); var cr = ((__color >> 16) & 0xFF) / 255.0; var cg = ((__color >> 8) & 0xFF) / 255.0; @@ -279,7 +279,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (i in 0...width * height) { - var f = field[i] * __strength; + var f = mask[i] * __strength; if (f > 1) f = 1; else if (f < 0) f = 0; fxR.push(cr * f); diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index afe5f0e5a9..c6eef0d7b1 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -87,8 +87,8 @@ import openfl.geom.Rectangle; var width = bitmapData.width; var height = bitmapData.height; - var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); - BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); if (__rampDirty) __buildRamp(); var ramp = __rampChannels(); @@ -104,8 +104,8 @@ import openfl.geom.Rectangle; { // signed bevel distance -> ramp index, as GradientBevelShader does: // -1 is one edge, 0 the (usually transparent) middle stop, +1 the other - var bL = BitmapFilter.__fieldAt(field, width, height, x + dx, y + dy); - var bR = BitmapFilter.__fieldAt(field, width, height, x - dx, y - dy); + var bL = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); + var bR = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); var sd = (bL - bR) * __strength; if (sd > 1) sd = 1; else if (sd < -1) sd = -1; diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index 3b07c52b0e..a895d00bab 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -90,9 +90,9 @@ import openfl.geom.Rectangle; var height = bitmapData.height; // blurred source alpha, read back shifted by distance/angle (as the GL path - // samples the field at coord - offset) - var field = BitmapFilter.__alphaField(sourceBitmapData, sourceRect, destPoint, width, height); - BitmapFilter.__blurField(field, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); + // samples the mask at coord - offset) + var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); + BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); if (__rampDirty) __buildRamp(); var ramp = __rampChannels(); @@ -102,11 +102,11 @@ import openfl.geom.Rectangle; { for (x in 0...width) { - var f = BitmapFilter.__fieldAt(field, width, height, x - Std.int(__offsetX * __renderScale), y - Std.int(__offsetY * __renderScale)) * __strength; + var f = BitmapFilter.__maskAt(mask, width, height, x - Std.int(__offsetX * __renderScale), y - Std.int(__offsetY * __renderScale)) * __strength; if (f > 1) f = 1; else if (f < 0) f = 0; - // index the ramp by the field, exactly as GradientGlowShader does + // index the ramp by the mask, exactly as GradientGlowShader does var i = Std.int(f * 255 + 0.5) * 4; fxR.push(ramp[i]); fxG.push(ramp[i + 1]); @@ -345,8 +345,8 @@ private class GradientGlowShader extends BitmapFilterShader void main(void) { vec4 src = texture2D(sourceBitmap, textureCoords.xy); - float field = texture2D(openfl_Texture, textureCoords.zw).a; - float f = clamp(field * uStrength, 0.0, 1.0); + float mask = texture2D(openfl_Texture, textureCoords.zw).a; + float f = clamp(mask * uStrength, 0.0, 1.0); // index the ramp by the distance field (high near the shape = inner // ratio 255, low far away = outer ratio 0), same for every type. From ad15e85ab535e3dc35d18d8b88c441129f1cecb0 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Thu, 3 Sep 2026 21:29:34 +0200 Subject: [PATCH 18/21] Bevel filters: name the mask samples and colours by meaning BevelFilter.__applyFilter: hr/hg/hb and sr/sg/sb become highlightR/G/B and shadowR/G/B (each premultiplied by its alpha, as uLightColor/uShadowColor). bL/bR (inherited "blur left/right" from BevelShader) become maskTowardShadow/maskTowardLight in BevelFilter and GradientBevelFilter (CPU path and the GradientBevelShader GLSL): the samples are taken along the light angle, +offset the way a shadow falls, -offset toward the light, so left/right was misleading for any angle other than 0. Comments explain the sign of the difference (positive = edge facing the light = highlight). Pure rename, no behaviour change. Co-Authored-By: Claude Fable 5.1 --- src/openfl/filters/BevelFilter.hx | 33 ++++++++++++++--------- src/openfl/filters/GradientBevelFilter.hx | 22 ++++++++------- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 15f369bca4..37feb03789 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -169,27 +169,36 @@ import lime._internal.graphics.ImageDataUtil; var dx = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); var dy = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); - var hr = (((__highlightColor >> 16) & 0xFF) / 255.0) * __highlightAlpha; - var hg = (((__highlightColor >> 8) & 0xFF) / 255.0) * __highlightAlpha; - var hb = ((__highlightColor & 0xFF) / 255.0) * __highlightAlpha; - var sr = (((__shadowColor >> 16) & 0xFF) / 255.0) * __shadowAlpha; - var sg = (((__shadowColor >> 8) & 0xFF) / 255.0) * __shadowAlpha; - var sb = ((__shadowColor & 0xFF) / 255.0) * __shadowAlpha; + // highlight / shadow colours, each channel premultiplied by its alpha + // the same values the shader receives as uLightColor / uShadowColor + var highlightR = (((__highlightColor >> 16) & 0xFF) / 255.0) * __highlightAlpha; + var highlightG = (((__highlightColor >> 8) & 0xFF) / 255.0) * __highlightAlpha; + var highlightB = ((__highlightColor & 0xFF) / 255.0) * __highlightAlpha; + var shadowR = (((__shadowColor >> 16) & 0xFF) / 255.0) * __shadowAlpha; + var shadowG = (((__shadowColor >> 8) & 0xFF) / 255.0) * __shadowAlpha; + var shadowB = ((__shadowColor & 0xFF) / 255.0) * __shadowAlpha; var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (y in 0...height) { for (x in 0...width) { - var bL = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); - var bR = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); - var d = (bL - bR) * __strength; + // Sample the blurred mask a short way along the light angle in both + // directions. Flash's `angle` is where the light comes from, so `+offset` + // points the way a shadow falls and `-offset` points toward the light. + // (called blurLeft / blurRight in BevelShader) + var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); + var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); + var d = (maskTowardShadow - maskTowardLight) * __strength; + // positive: more shape toward the shadow than toward the light, so this + // pixel sits on the edge FACING the light -> highlight. negative -> the + // shadow-facing edge. var high = d > 1 ? 1.0 : (d < 0 ? 0.0 : d); var shad = -d > 1 ? 1.0 : (-d < 0 ? 0.0 : -d); - fxR.push(hr * high + sr * shad); - fxG.push(hg * high + sg * shad); - fxB.push(hb * high + sb * shad); + fxR.push(highlightR * high + shadowR * shad); + fxG.push(highlightG * high + shadowG * shad); + fxB.push(highlightB * high + shadowB * shad); fxA.push(__highlightAlpha * high + __shadowAlpha * shad); } } diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index c6eef0d7b1..2fd19a487d 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -104,9 +104,11 @@ import openfl.geom.Rectangle; { // signed bevel distance -> ramp index, as GradientBevelShader does: // -1 is one edge, 0 the (usually transparent) middle stop, +1 the other - var bL = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); - var bR = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); - var sd = (bL - bR) * __strength; + // mask sampled along the light angle: +offset is the way a shadow falls, + // -offset points toward the light (Flash's `angle` is where light comes FROM) + var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); + var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); + var sd = (maskTowardShadow - maskTowardLight) * __strength; if (sd > 1) sd = 1; else if (sd < -1) sd = -1; @@ -349,16 +351,16 @@ private class GradientBevelShader extends BitmapFilterShader void main(void) { vec4 dest = texture2D(sourceBitmap, vTextureCoord); - vec2 uvL = vTextureCoord + vTransform; - vec2 uvR = vTextureCoord - vTransform; - float bL = texture2D(openfl_Texture, uvL).a; - float bR = texture2D(openfl_Texture, uvR).a; - if (uvL.x<0.0||uvL.x>1.0||uvL.y<0.0||uvL.y>1.0) bL = 0.0; - if (uvR.x<0.0||uvR.x>1.0||uvR.y<0.0||uvR.y>1.0) bR = 0.0; + vec2 uvTowardShadow = vTextureCoord + vTransform; + vec2 uvTowardLight = vTextureCoord - vTransform; + float maskTowardShadow = texture2D(openfl_Texture, uvTowardShadow).a; + float maskTowardLight = texture2D(openfl_Texture, uvTowardLight).a; + if (uvTowardShadow.x<0.0||uvTowardShadow.x>1.0||uvTowardShadow.y<0.0||uvTowardShadow.y>1.0) maskTowardShadow = 0.0; + if (uvTowardLight.x<0.0||uvTowardLight.x>1.0||uvTowardLight.y<0.0||uvTowardLight.y>1.0) maskTowardLight = 0.0; // signed distance field -> ramp index (-1 = one edge/ratio 0, // 0 = base/ratio 128, +1 = other edge/ratio 255) - float sd = clamp((bL - bR) * uStrength, -1.0, 1.0); + float sd = clamp((maskTowardShadow - maskTowardLight) * uStrength, -1.0, 1.0); vec4 glow = texture2D(gradientRamp, vec2(sd * 0.5 + 0.5, 0.5)); if (uBevelType == 0.0) { From 26a0d0ed309fe843782f8d8c7704ec09ec68aa4f Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Thu, 3 Sep 2026 21:30:46 +0200 Subject: [PATCH 19/21] refactoring --- src/openfl/filters/BevelFilter.hx | 5 ++--- src/openfl/filters/BlurFilter.hx | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index 37feb03789..df7bc0f590 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -159,9 +159,8 @@ import lime._internal.graphics.ImageDataUtil; var width = bitmapData.width; var height = bitmapData.height; - // Same derivation as BevelShader: blur the source alpha, then compare the - // mask either side of the light direction. The signed difference drives the - // highlight (light side) and the shadow (dark side). + // Same Shader: blur the source alpha, then compare the mask either side of the light direction. + // The signed difference drives the highlight (light side) and the shadow (dark side). var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index cf457c2a7e..d7a8c83527 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -271,7 +271,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private function set_quality(value:Int):Int { - // TODO: Quality effect with fewer passes? // one horizontal + one vertical box pass per quality iteration var passes = (value > 0) ? value : 1; From 27b6c53ec9fe4577964b2db84b138e20de2e0538 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Thu, 3 Sep 2026 22:04:48 +0200 Subject: [PATCH 20/21] refactoring --- src/openfl/filters/BevelFilter.hx | 22 +++-- src/openfl/filters/BitmapFilter.hx | 125 +++++++++++++---------------- src/openfl/filters/BlurFilter.hx | 10 +-- 3 files changed, 73 insertions(+), 84 deletions(-) diff --git a/src/openfl/filters/BevelFilter.hx b/src/openfl/filters/BevelFilter.hx index df7bc0f590..fa14269a08 100644 --- a/src/openfl/filters/BevelFilter.hx +++ b/src/openfl/filters/BevelFilter.hx @@ -188,17 +188,15 @@ import lime._internal.graphics.ImageDataUtil; // (called blurLeft / blurRight in BevelShader) var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); - var d = (maskTowardShadow - maskTowardLight) * __strength; - // positive: more shape toward the shadow than toward the light, so this - // pixel sits on the edge FACING the light -> highlight. negative -> the - // shadow-facing edge. - var high = d > 1 ? 1.0 : (d < 0 ? 0.0 : d); - var shad = -d > 1 ? 1.0 : (-d < 0 ? 0.0 : -d); - - fxR.push(highlightR * high + shadowR * shad); - fxG.push(highlightG * high + shadowG * shad); - fxB.push(highlightB * high + shadowB * shad); - fxA.push(__highlightAlpha * high + __shadowAlpha * shad); + var dist = (maskTowardShadow - maskTowardLight) * __strength; + + var highlight = dist > 1 ? 1.0 : (dist < 0 ? 0.0 : dist); + var shadow = -dist > 1 ? 1.0 : (-dist < 0 ? 0.0 : -dist); + + fxR.push(highlightR * highlight + shadowR * shadow); + fxG.push(highlightG * highlight + shadowG * shadow); + fxB.push(highlightB * highlight + shadowB * shadow); + fxA.push(__highlightAlpha * highlight + __shadowAlpha * shadow); } } @@ -478,7 +476,7 @@ import lime._internal.graphics.ImageDataUtil; var offsetX:Int = __type != "inner" ? Math.ceil(__distance * Math.cos(__angle * Math.PI / 180)) : 0; var offsetY:Int = __type != "inner" ? Math.ceil(__distance * Math.sin(__angle * Math.PI / 180)) : 0; // Box blur reach grows to ~quality*blur/2 per side (see DropShadowFilter); - // reserve the full spread so the bevel isn't clipped at high quality. + // reserving the full spread so the bevel isn't clipped at high quality. var q = (__quality > 0) ? __quality : 1; var exX = Math.ceil(__blurX * 0.5 * q) + 4; var exY = Math.ceil(__blurY * 0.5 * q) + 4; diff --git a/src/openfl/filters/BitmapFilter.hx b/src/openfl/filters/BitmapFilter.hx index 75d8cbd6d0..fe6c712919 100644 --- a/src/openfl/filters/BitmapFilter.hx +++ b/src/openfl/filters/BitmapFilter.hx @@ -38,13 +38,6 @@ class BitmapFilter /** Whether `__applyFilter` composites the original object into its own result. - - The GPU path hands the unfiltered object to the combine shader (see - `__initShader`), which composites inner / knockout / full itself. The - software path instead relies on the caller drawing the object back on top - afterwards, which can only ever produce an "outer, non-knockout" result. - Filters whose `__applyFilter` does the whole composite set this so the - software callers skip that draw. The GPU path never reads it. **/ @:noCompletion private var __softwareComposite:Bool; @@ -64,7 +57,6 @@ class BitmapFilter public function new() { - __renderScale = 1; __bottomExtension = 0; __leftExtension = 0; __needSecondBitmapData = true; @@ -74,6 +66,7 @@ class BitmapFilter __shaderBlendMode = NORMAL; __topExtension = 0; __smooth = true; + __renderScale = 1; __softwareComposite = false; } @@ -99,17 +92,6 @@ class BitmapFilter return null; } - // ------------------------------------------------------------------------ - // Software (CPU) helpers, shared by the effect filters. These mirror the GL - // shaders so both paths produce the same image: the same fractional box blur - // of the source alpha, and the same combine formulas. - // - // "mask" throughout = the shape's alpha as one Float (0..1) per pixel of the - // destination grid. Blurred, it becomes a soft coverage map -- 1 inside the - // shape, fading to 0 outside -- and that is the geometry every effect is - // derived from: glow/shadow read it, bevel takes its directional difference, - // the gradient filters index a colour ramp by it. - // ------------------------------------------------------------------------ /** The source's alpha channel as a 0..1 mask laid out on the destination @@ -119,13 +101,13 @@ class BitmapFilter **/ @:noCompletion private static function __alphaMask(source:BitmapData, sourceRect:Rectangle, destPoint:Point, width:Int, height:Int):Array { - var mask = new Array(); - for (i in 0...width * height) - mask.push(0.0); + var mask = [for (i in 0...width * height) 0.0]; var pixels = source.getVector(sourceRect); + var sw = Std.int(sourceRect.width); var sh = Std.int(sourceRect.height); + var ox = Std.int(destPoint.x); var oy = Std.int(destPoint.y); @@ -150,9 +132,7 @@ class BitmapFilter @:noCompletion private static function __blurMask(mask:Array, width:Int, height:Int, blurX:Float, blurY:Float, quality:Int):Array { var passes = (quality > 0) ? quality : 1; - var scratch = new Array(); - for (i in 0...mask.length) - scratch.push(0.0); + var scratch = [for (i in 0...mask.length) 0.0]; for (i in 0...passes) { @@ -222,8 +202,13 @@ class BitmapFilter var height = dest.height; // source pixels on the destination grid, premultiplied - var srcR = new Array(), srcG = new Array(), srcB = new Array(), srcA = new Array(); - for (i in 0...width * height) + var srcR = new Array(); + var srcG = new Array(); + var srcB = new Array(); + var srcA = new Array(); + + var pixelCount = width * height; + for (i in 0...pixelCount) { srcR.push(0.0); srcG.push(0.0); @@ -232,22 +217,25 @@ class BitmapFilter } var pixels = source.getVector(sourceRect); - var sw = Std.int(sourceRect.width); - var sh = Std.int(sourceRect.height); - var ox = Std.int(destPoint.x); - var oy = Std.int(destPoint.y); - for (y in 0...sh) + var sourceWidth = Std.int(sourceRect.width); + var sourceHeight = Std.int(sourceRect.height); + var destOffsetX = Std.int(destPoint.x); + var destOffsetY = Std.int(destPoint.y); + + for (y in 0...sourceHeight) { - var dy = y + oy; - if (dy < 0 || dy >= height) continue; - for (x in 0...sw) + var destY = y + destOffsetY; + if (destY < 0 || destY >= height) continue; + + for (x in 0...sourceWidth) { - var dx = x + ox; - if (dx < 0 || dx >= width) continue; - var argb = pixels[y * sw + x]; + var destX = x + destOffsetX; + if (destX < 0 || destX >= width) continue; + + var argb = pixels[y * sourceWidth + x]; var a = ((argb >>> 24) & 0xFF) / 255.0; - var i = dy * width + dx; + var i = destY * width + destX; srcA[i] = a; srcR[i] = (((argb >> 16) & 0xFF) / 255.0) * a; srcG[i] = (((argb >> 8) & 0xFF) / 255.0) * a; @@ -259,61 +247,62 @@ class BitmapFilter for (i in 0...width * height) { - var sr = srcR[i], sg = srcG[i], sb = srcB[i], sa = srcA[i]; - var er = fxR[i], eg = fxG[i], eb = fxB[i], ea = fxA[i]; + var sourceR = srcR[i], sourceG = srcG[i], sourceB = srcB[i], sourceA = srcA[i]; + var effectR = fxR[i], effectG = fxG[i], effectB = fxB[i], effectA = fxA[i]; var r:Float, g:Float, b:Float, a:Float; if (type == INNER) { - var mr = er * sa, mg = eg * sa, mb = eb * sa, ma = ea * sa; + // the effect confined to the shape: scaled by the source alpha + var maskedR = effectR * sourceA, maskedG = effectG * sourceA, maskedB = effectB * sourceA, maskedA = effectA * sourceA; if (knockout) { - r = mr; - g = mg; - b = mb; - a = ma; + r = maskedR; + g = maskedG; + b = maskedB; + a = maskedA; } else { - r = sr * (1 - ea) + mr; - g = sg * (1 - ea) + mg; - b = sb * (1 - ea) + mb; - a = sa; + r = sourceR * (1 - effectA) + maskedR; + g = sourceG * (1 - effectA) + maskedG; + b = sourceB * (1 - effectA) + maskedB; + a = sourceA; } } else if (type == FULL) { if (knockout) { - r = er; - g = eg; - b = eb; - a = ea; + r = effectR; + g = effectG; + b = effectB; + a = effectA; } else { - r = sr * (1 - ea) + er; - g = sg * (1 - ea) + eg; - b = sb * (1 - ea) + eb; - a = sa * (1 - ea) + ea; + r = sourceR * (1 - effectA) + effectR; + g = sourceG * (1 - effectA) + effectG; + b = sourceB * (1 - effectA) + effectB; + a = sourceA * (1 - effectA) + effectA; } } else // OUTER { - var k = 1 - sa; + var outside = 1 - sourceA; // how much of this pixel lies outside the shape if (knockout) { - r = er * k; - g = eg * k; - b = eb * k; - a = ea * k; + r = effectR * outside; + g = effectG * outside; + b = effectB * outside; + a = effectA * outside; } else { - r = sr + er * k; - g = sg + eg * k; - b = sb + eb * k; - a = sa + ea * k; + r = sourceR + effectR * outside; + g = sourceG + effectG * outside; + b = sourceB + effectB * outside; + a = sourceA + effectA * outside; } } @@ -329,10 +318,12 @@ class BitmapFilter { if (a <= 0) return 0; if (a > 1) a = 1; + var ir = Std.int(__clamp01(r / a) * 255 + 0.5); var ig = Std.int(__clamp01(g / a) * 255 + 0.5); var ib = Std.int(__clamp01(b / a) * 255 + 0.5); var ia = Std.int(a * 255 + 0.5); + return (ia << 24) | (ir << 16) | (ig << 8) | ib; } diff --git a/src/openfl/filters/BlurFilter.hx b/src/openfl/filters/BlurFilter.hx index d7a8c83527..892b9e53ab 100644 --- a/src/openfl/filters/BlurFilter.hx +++ b/src/openfl/filters/BlurFilter.hx @@ -206,11 +206,11 @@ import lime._internal.graphics.ImageDataUtil; // TODO // Configure the box-blur shader for one axis of one pass. @:noCompletion private static function __setupBlurShader(horizontal:Bool, v:Float):BitmapFilterShader { - var s = __blurShader; - s.uDir.value[0] = horizontal ? 1.0 : 0.0; - s.uDir.value[1] = horizontal ? 0.0 : 1.0; - s.uFullSize.value[0] = v > 255 ? 255.0 : v; - return s; + var shader = __blurShader; + shader.uDir.value[0] = horizontal ? 1.0 : 0.0; + shader.uDir.value[1] = horizontal ? 0.0 : 1.0; + shader.uFullSize.value[0] = v > 255 ? 255.0 : v; + return shader; } @:noCompletion inline function __padFor(value:Float):Int From 0ef10fb2490b09ab14e7d925f01cfe4e1f27c5f6 Mon Sep 17 00:00:00 2001 From: m0rkeulv Date: Thu, 3 Sep 2026 22:40:13 +0200 Subject: [PATCH 21/21] refactoring --- src/openfl/filters/DropShadowFilter.hx | 41 ++++++++++++----------- src/openfl/filters/GlowFilter.hx | 25 ++++++-------- src/openfl/filters/GradientBevelFilter.hx | 34 +++++++++---------- src/openfl/filters/GradientGlowFilter.hx | 25 +++++++------- 4 files changed, 59 insertions(+), 66 deletions(-) diff --git a/src/openfl/filters/DropShadowFilter.hx b/src/openfl/filters/DropShadowFilter.hx index 54390a8e4c..ce35329965 100644 --- a/src/openfl/filters/DropShadowFilter.hx +++ b/src/openfl/filters/DropShadowFilter.hx @@ -282,8 +282,8 @@ import lime._internal.graphics.ImageDataUtil; // TODO __needSecondBitmapData = true; __preserveObject = true; - __softwareComposite = true; __renderDirty = true; + __softwareComposite = true; } public override function clone():BitmapFilter @@ -296,10 +296,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO var width = bitmapData.width; var height = bitmapData.height; - // blur the source alpha, then read it back shifted by the shadow offset -- - // the GL path does the same by sampling the glow at (coord - offset). - // An inner shadow blurs the *inverted* alpha (the GL path runs - // InvertAlphaShader as its first pass) so the shadow falls inside the edge. + // blur the source alpha, then read it back shifted by the shadow offset var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); if (__inner) { @@ -308,26 +305,30 @@ import lime._internal.graphics.ImageDataUtil; // TODO } BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); - var cr = ((__color >> 16) & 0xFF) / 255.0; - var cg = ((__color >> 8) & 0xFF) / 255.0; - var cb = (__color & 0xFF) / 255.0; - var ox = Std.int(__offsetX * __renderScale); - var oy = Std.int(__offsetY * __renderScale); + var colorR = ((__color >> 16) & 0xFF) / 255.0; + var colorG = ((__color >> 8) & 0xFF) / 255.0; + var colorB = (__color & 0xFF) / 255.0; + + var shadowOffsetX = Std.int(__offsetX * __renderScale); + var shadowOffsetY = Std.int(__offsetY * __renderScale); var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (y in 0...height) { for (x in 0...width) { - var sx = x - ox; - var sy = y - oy; - var f = ((sx < 0 || sx >= width || sy < 0 || sy >= height) ? 0.0 : mask[sy * width + sx]) * __strength; - if (f > 1) f = 1; - else if (f < 0) f = 0; - fxR.push(cr * f); - fxG.push(cg * f); - fxB.push(cb * f); - fxA.push(__alpha * f); + // the shadow at this pixel is the blurred mask read from `offset` pixels back + var maskX = x - shadowOffsetX; + var maskY = y - shadowOffsetY; + var shadowCoverage = ((maskX < 0 || maskX >= width || maskY < 0 || maskY >= height) ? 0.0 : mask[maskY * width + maskX]) * __strength; + + if (shadowCoverage > 1) shadowCoverage = 1; + else if (shadowCoverage < 0) shadowCoverage = 0; + + fxR.push(colorR * shadowCoverage); + fxG.push(colorG * shadowCoverage); + fxB.push(colorB * shadowCoverage); + fxA.push(__alpha * shadowCoverage); } } @@ -561,7 +562,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { __renderDirty = true; __quality = value; - __updateSize(); // passes & extension both depend on quality + __updateSize(); // quality affects the size } return __quality = value; } diff --git a/src/openfl/filters/GlowFilter.hx b/src/openfl/filters/GlowFilter.hx index 4804928182..7e82836292 100644 --- a/src/openfl/filters/GlowFilter.hx +++ b/src/openfl/filters/GlowFilter.hx @@ -261,9 +261,6 @@ import lime._internal.graphics.ImageDataUtil; // TODO var height = bitmapData.height; // blur the source alpha into the glow's coverage mask, then colourise it - // exactly as BoxBlurAlphaShader does: fx = color * clamp(mask * strength). - // An inner glow blurs the *inverted* alpha (the GL path runs InvertAlphaShader - // as its first pass), so the glow grows inwards from the edge. var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); if (__inner) { @@ -368,16 +365,16 @@ import lime._internal.graphics.ImageDataUtil; // TODO @:noCompletion private static function __setupBlurAlphaShader(horizontal:Bool, v:Float, color:Int, alpha:Float, strength:Float):BitmapFilterShader { - var s = __blurAlphaShader; - s.uDir.value[0] = horizontal ? 1.0 : 0.0; - s.uDir.value[1] = horizontal ? 0.0 : 1.0; - s.uFullSize.value[0] = v > 255 ? 255.0 : v; - s.uColor.value[0] = ((color >> 16) & 0xFF) / 255; - s.uColor.value[1] = ((color >> 8) & 0xFF) / 255; - s.uColor.value[2] = (color & 0xFF) / 255; - s.uColor.value[3] = alpha; - s.uStrength.value[0] = strength; - return s; + var shader = __blurAlphaShader; + shader.uDir.value[0] = horizontal ? 1.0 : 0.0; + shader.uDir.value[1] = horizontal ? 0.0 : 1.0; + shader.uFullSize.value[0] = v > 255 ? 255.0 : v; + shader.uColor.value[0] = ((color >> 16) & 0xFF) / 255; + shader.uColor.value[1] = ((color >> 8) & 0xFF) / 255; + shader.uColor.value[2] = (color & 0xFF) / 255; + shader.uColor.value[3] = alpha; + shader.uStrength.value[0] = strength; + return shader; } // Get & Set Methods @@ -476,7 +473,7 @@ import lime._internal.graphics.ImageDataUtil; // TODO { __renderDirty = true; __quality = value; - __updateSize(); // passes & extension both depend on quality + __updateSize(); // quality affects the size } return __quality = value; } diff --git a/src/openfl/filters/GradientBevelFilter.hx b/src/openfl/filters/GradientBevelFilter.hx index 2fd19a487d..7016dedbe5 100644 --- a/src/openfl/filters/GradientBevelFilter.hx +++ b/src/openfl/filters/GradientBevelFilter.hx @@ -94,25 +94,26 @@ import openfl.geom.Rectangle; var ramp = __rampChannels(); var rad = __angle * Math.PI / 180; - var dx = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); - var dy = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); + var offsetX = Std.int(Math.round(__distance * Math.cos(rad) * __renderScale)); + var offsetY = Std.int(Math.round(__distance * Math.sin(rad) * __renderScale)); var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (y in 0...height) { for (x in 0...width) { - // signed bevel distance -> ramp index, as GradientBevelShader does: - // -1 is one edge, 0 the (usually transparent) middle stop, +1 the other // mask sampled along the light angle: +offset is the way a shadow falls, // -offset points toward the light (Flash's `angle` is where light comes FROM) - var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + dx, y + dy); - var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - dx, y - dy); - var sd = (maskTowardShadow - maskTowardLight) * __strength; - if (sd > 1) sd = 1; - else if (sd < -1) sd = -1; + var maskTowardShadow = BitmapFilter.__maskAt(mask, width, height, x + offsetX, y + offsetY); + var maskTowardLight = BitmapFilter.__maskAt(mask, width, height, x - offsetX, y - offsetY); + // signed edge slope -> ramp index, as GradientBevelShader does: positive on + // the edge facing the light, negative on the far edge, zero on flat areas. + // -1 is one edge, 0 the (usually transparent) middle stop, +1 the other + var edgeSlope = (maskTowardShadow - maskTowardLight) * __strength; + if (edgeSlope > 1) edgeSlope = 1; + else if (edgeSlope < -1) edgeSlope = -1; - var i = Std.int((sd * 0.5 + 0.5) * 255 + 0.5) * 4; + var i = Std.int((edgeSlope * 0.5 + 0.5) * 255 + 0.5) * 4; fxR.push(ramp[i]); fxG.push(ramp[i + 1]); fxB.push(ramp[i + 2]); @@ -123,8 +124,6 @@ import openfl.geom.Rectangle; return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __type, __knockout); } - // The 256-entry ramp as flat premultiplied [r,g,b,a] floats, matching how the - // ramp BitmapData is premultiplied when uploaded as a texture on the GL path. @:noCompletion private function __rampChannels():Array { var out = new Array(); @@ -168,9 +167,8 @@ import openfl.geom.Rectangle; #end } - // Build the 256-entry straight-ARGB gradient ramp (one texel per output index - // 0..255) from the (colors, alphas, ratios) stops. Each index is the colour and - // alpha linearly interpolated between the two stops it falls between. + // 256-entry straight-ARGB gradient ramp (one texel per output index 0..255) from the (colors, alphas, ratios). + // Each index is the colour and alpha linearly interpolated between the two stops it falls between. @:noCompletion private function __buildRamp():Void { if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); @@ -180,8 +178,7 @@ import openfl.geom.Rectangle; for (index in 0...256) { // advance to the stop pair whose ratio range contains `index` - while (stop < stopCount - 1 && __ratios[stop + 1] < index) - stop++; + while (stop < stopCount - 1 && __ratios[stop + 1] < index) stop++; var colorLo = __colors[stop]; var alphaLo = __alphas[stop]; @@ -202,8 +199,7 @@ import openfl.geom.Rectangle; var colorHi = __colors[stop + 1]; var alphaHi = __alphas[stop + 1]; - // blend = how far `index` sits between the two stops (0 at the low - // stop, 1 at the high stop) + // blend = how far `index` sits between the two stops (0 at the low stop, 1 at the high stop) var blend = (ratioHi > ratioLo) ? (index - ratioLo) / (ratioHi - ratioLo) : 0.0; r = lerp((colorLo >> 16) & 0xFF, (colorHi >> 16) & 0xFF, blend); diff --git a/src/openfl/filters/GradientGlowFilter.hx b/src/openfl/filters/GradientGlowFilter.hx index a895d00bab..e17f933e9b 100644 --- a/src/openfl/filters/GradientGlowFilter.hx +++ b/src/openfl/filters/GradientGlowFilter.hx @@ -89,25 +89,27 @@ import openfl.geom.Rectangle; var width = bitmapData.width; var height = bitmapData.height; - // blurred source alpha, read back shifted by distance/angle (as the GL path - // samples the mask at coord - offset) + // blurred source alpha, read back shifted by distance/angle (as the GL path samples the mask at coord - offset) var mask = BitmapFilter.__alphaMask(sourceBitmapData, sourceRect, destPoint, width, height); BitmapFilter.__blurMask(mask, width, height, __blurX * __renderScale, __blurY * __renderScale, __quality); if (__rampDirty) __buildRamp(); var ramp = __rampChannels(); + var glowOffsetX = Std.int(__offsetX * __renderScale); + var glowOffsetY = Std.int(__offsetY * __renderScale); + var fxR = new Array(), fxG = new Array(), fxB = new Array(), fxA = new Array(); for (y in 0...height) { for (x in 0...width) { - var f = BitmapFilter.__maskAt(mask, width, height, x - Std.int(__offsetX * __renderScale), y - Std.int(__offsetY * __renderScale)) * __strength; - if (f > 1) f = 1; - else if (f < 0) f = 0; + var glowCoverage = BitmapFilter.__maskAt(mask, width, height, x - glowOffsetX, y - glowOffsetY) * __strength; + if (glowCoverage > 1) glowCoverage = 1; + else if (glowCoverage < 0) glowCoverage = 0; // index the ramp by the mask, exactly as GradientGlowShader does - var i = Std.int(f * 255 + 0.5) * 4; + var i = Std.int(glowCoverage * 255 + 0.5) * 4; fxR.push(ramp[i]); fxG.push(ramp[i + 1]); fxB.push(ramp[i + 2]); @@ -118,8 +120,7 @@ import openfl.geom.Rectangle; return BitmapFilter.__compositeEffect(bitmapData, sourceBitmapData, sourceRect, destPoint, fxR, fxG, fxB, fxA, __type, __knockout); } - // The 256-entry ramp as flat premultiplied [r,g,b,a] floats, matching how the - // ramp BitmapData is premultiplied when uploaded as a texture on the GL path. + // 256-entry ramp as flat premultiplied [r,g,b,a] floats, matching how the ramp BitmapData is premultiplied when uploaded as a texture. @:noCompletion private function __rampChannels():Array { var out = new Array(); @@ -164,9 +165,8 @@ import openfl.geom.Rectangle; #end } - // Build the 256-entry straight-ARGB gradient ramp (one texel per output index - // 0..255) from the (colors, alphas, ratios) stops. Each index is the colour and - // alpha linearly interpolated between the two stops it falls between. + // Build the 256-entry straight-ARGB gradient ramp (one texel per output index 0..255) from the (colors, alphas, ratios). + // Each index is the colour and alpha linearly interpolated between the two stops it falls between. @:noCompletion private function __buildRamp():Void { if (__ramp == null) __ramp = new BitmapData(256, 1, true, 0); @@ -176,8 +176,7 @@ import openfl.geom.Rectangle; for (index in 0...256) { // advance to the stop pair whose ratio range contains `index` - while (stop < stopCount - 1 && __ratios[stop + 1] < index) - stop++; + while (stop < stopCount - 1 && __ratios[stop + 1] < index) stop++; var colorLo = __colors[stop]; var alphaLo = __alphas[stop];