DropShadow/Glow/Blur Filters issue (Software only)

I’ve decided to create a topic for this issue since I’ve initially pull-requested some changes but I’ve found out a better solution to fix all the filters at once. This issue was once mentioned in another topic: Problems with TextFields with DropShadow filter and I also need dynamic TextFields with .text changing while having DropShadowFilter or GlowFilter applied to them. I looked into ImageDataUtil.gaussianBlur and found out that this implementation is actually described on a page I’ve looked up in Google: http://blog.ivank.net/fastest-gaussian-blur.html - it’s super great because it has linear time complexity and speeds up blur a lot. The only thing is that it modifies not only the target bitmapData but also modifies the source bitmapData (ex. bitmapData.image.data) (which is mentioned also in TODO comment at the beginning) within the box blurs methods.

At this point I’ve fixed all three filters - the only thing I’m trying to solve is the initial form of DropShadowFilter which gets correct after the first .text property change thus after the second time being rendered (due to __offsetX/__offsetY properties).

What I’m not sure of is whether my approach to resolve issue with writing to both source and target bitmapData is correct - I have to clone sourceBitmapData to achieve that. For example GlowFilter.__applyFilter method initially looked:

		var r = (__color >> 16) & 0xFF;
		var g = (__color >> 8) & 0xFF;
		var b = __color & 0xFF;
		sourceBitmapData.colorTransform (sourceBitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
		
		var finalImage = ImageDataUtil.gaussianBlur (bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);
		
		if (finalImage == bitmapData.image) return bitmapData;
		return sourceBitmapData;

and I’ve changed it to:

		var r = (__color >> 16) & 0xFF;
		var g = (__color >> 8) & 0xFF;
		var b = __color & 0xFF;
		bitmapData.colorTransform (bitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
		
		var finalImage = ImageDataUtil.gaussianBlur (sourceBitmapData.clone().image, bitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);
		return finalImage == sourceBitmapData.image ? sourceBitmapData : bitmapData;

I’ve switched bitmapData nad sourceBitmapData places because in described method the first image is source and the second is target and I’ve added clone() to sourceBitmapData

DropShadowFilter (with my modifications from pull-request https://github.com/openfl/openfl/pull/2030) looks like this (now with __needSecondBitmapData property set back to true instead of what I proposed in PR):

@:noCompletion private override function __applyFilter (bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData {
		
		// TODO: Support knockout, inner
		
		var r = (__color >> 16) & 0xFF;
		var g = (__color >> 8) & 0xFF;
		var b = __color & 0xFF;
		bitmapData.colorTransform (bitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
		
		destPoint.x += __offsetX;
		destPoint.y += __offsetY;
		
		var finalImage = ImageDataUtil.gaussianBlur (sourceBitmapData.clone().image, bitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);

		destPoint.x = __offsetX;
		destPoint.y = __offsetY;

		return finalImage == sourceBitmapData.image ? sourceBitmapData : bitmapData;
		
	}

I’m testing this on HTML5 target, DOM/Canvas/Cairo and looks great on all of these.

OK. Got it working fully. Here is the effect in HTML5 -Dcanvas on the left against FLASH on the right with my changes applied:

And here is before the changes, original implementation:

The difference may be unnoticeable but apart from having issues described above fixed, first zeros are not aligned in the original version (shadow is aligned with the original image and the actual text is offseted by the shadow’s distance offsets calculated within filter). Flash does keep texts aligned while the shadow itself is offseted. Glow (aka Outline here because I’ve set 150.0 strength to actually have an outline effect) is the same.

I’ve got diffs prepped for these changes so I can provide them any time (or fork and PR both Lime and OpenFL).

Great find! Yes, I have a problem with the gaussian blur implementation we have. I’m hoping that someone will be able to come up with a more solid/reliable/representative gaussian blur implementation, which really effects the quality of all these effects

I’m not really super into math behind Gaussian Blur but this implementation has good theory behind it as it was measured in the link I’ve found - it’s really fast compared to the standard implementation - that modification of both source and target is the disadvantage but comparing its time to other implementations it is ahead a lot

I’d love to hear your opinion on that clone() i’ve mentioned and shown on code samples - from my perspective, at least for now it is mandatory for these filters to work correctly but I’m not sure whether that may be any problem. I can look into algorithm itself tomorrow since I’ll get some spare time to work on these things.

For now here are two of my patches for OpenFL and Lime respectively from today’s researches - you can check them out and apply them if you want:

For OpenFL:

Index: src/openfl/filters/DropShadowFilter.hx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/openfl/filters/DropShadowFilter.hx	(revision c7a2800757565a565ebead9ab952e25660128c61)
+++ src/openfl/filters/DropShadowFilter.hx	(revision )
@@ -268,19 +268,12 @@
 		var r = (__color >> 16) & 0xFF;
 		var g = (__color >> 8) & 0xFF;
 		var b = __color & 0xFF;
-		sourceBitmapData.colorTransform (sourceBitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
-		
-		destPoint.x += __offsetX;
-		destPoint.y += __offsetY;
+		bitmapData.colorTransform (bitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
+
+		var point:Point = new Point(__offsetX, __offsetY);
+		var finalImage = ImageDataUtil.gaussianBlur (sourceBitmapData.clone().image, bitmapData.image, sourceRect.__toLimeRectangle (), point.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);
+		return finalImage == sourceBitmapData.image ? sourceBitmapData : bitmapData;
 		
-		var finalImage = ImageDataUtil.gaussianBlur (bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);
-		
-		destPoint.x = __offsetX;
-		destPoint.y = __offsetY;
-		
-		if (finalImage == bitmapData.image) return bitmapData;
-		return sourceBitmapData;
-		
 	}
 	
 	
Index: src/openfl/filters/GlowFilter.hx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/openfl/filters/GlowFilter.hx	(revision c7a2800757565a565ebead9ab952e25660128c61)
+++ src/openfl/filters/GlowFilter.hx	(revision )
@@ -240,12 +240,10 @@
 		var r = (__color >> 16) & 0xFF;
 		var g = (__color >> 8) & 0xFF;
 		var b = __color & 0xFF;
-		sourceBitmapData.colorTransform (sourceBitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
+		bitmapData.colorTransform (bitmapData.rect, new ColorTransform (0, 0, 0, __alpha, r, g, b, 0));
 		
-		var finalImage = ImageDataUtil.gaussianBlur (bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);
-		
-		if (finalImage == bitmapData.image) return bitmapData;
-		return sourceBitmapData;
+		var finalImage = ImageDataUtil.gaussianBlur (sourceBitmapData.clone().image, bitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality, __strength);
+		return finalImage == sourceBitmapData.image ? sourceBitmapData : bitmapData;
 		
 	}
 	
Index: src/openfl/filters/BlurFilter.hx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/openfl/filters/BlurFilter.hx	(revision c7a2800757565a565ebead9ab952e25660128c61)
+++ src/openfl/filters/BlurFilter.hx	(revision )
@@ -188,10 +188,9 @@
 	
 	@:noCompletion private override function __applyFilter (bitmapData:BitmapData, sourceBitmapData:BitmapData, sourceRect:Rectangle, destPoint:Point):BitmapData {
 		
-		var finalImage = ImageDataUtil.gaussianBlur (bitmapData.image, sourceBitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality);
-		if (finalImage == bitmapData.image) return bitmapData;
-		return sourceBitmapData;
-		
+		var finalImage = ImageDataUtil.gaussianBlur (sourceBitmapData.clone().image, bitmapData.image, sourceRect.__toLimeRectangle (), destPoint.__toLimeVector2 (), __blurX, __blurY, __quality);
+		return finalImage == sourceBitmapData.image ? sourceBitmapData : bitmapData;
+
 	}
 	
 	
Index: src/openfl/display/DisplayObject.hx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/openfl/display/DisplayObject.hx	(revision c7a2800757565a565ebead9ab952e25660128c61)
+++ src/openfl/display/DisplayObject.hx	(revision )
@@ -2394,7 +2394,7 @@
 								bitmap3.copyPixels (bitmap, bitmap.rect, destPoint);
 							}
 							
-							lastBitmap = filter.__applyFilter (bitmap2, bitmap, sourceRect, destPoint);
+							lastBitmap = filter.__applyFilter (bitmap, bitmap2, sourceRect, destPoint);
 							
 							if (filter.__preserveObject) {
 								lastBitmap.draw (bitmap3, null, __objectTransform != null ? __objectTransform.colorTransform : null);

For Lime:

Index: src/lime/_internal/graphics/ImageDataUtil.hx
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- src/lime/_internal/graphics/ImageDataUtil.hx	(revision 4a8125e03641d1dc7bf18b6e6d779b1d24e7d3cb)
+++ src/lime/_internal/graphics/ImageDataUtil.hx	(revision )
@@ -507,15 +507,15 @@
 	}
 
 
-	public static function gaussianBlur (image:Image, sourceImage:Image, sourceRect:Rectangle, destPoint:Vector2, blurX:Float = 4, blurY:Float = 4, quality:Int = 1, strength:Float = 1):Image {
+	public static function gaussianBlur (sourceImage:Image, destImage:Image, sourceRect:Rectangle, destPoint:Vector2, blurX:Float = 4, blurY:Float = 4, quality:Int = 1, strength:Float = 1):Image {
 
 		// TODO: Support sourceRect better, do not modify sourceImage, create C++ implementation for native
 
 		// TODO: Faster approach
-		var imagePremultiplied = image.premultiplied;
 		var sourceImagePremultiplied = sourceImage.premultiplied;
-		if (imagePremultiplied) image.premultiplied = false;
+		var destImagePremultiplied = destImage.premultiplied;
 		if (sourceImagePremultiplied) sourceImage.premultiplied = false;
+		if (destImagePremultiplied) destImage.premultiplied = false;
 
 		// if (image.buffer.premultiplied || sourceImage.buffer.premultiplied) {
 		// 	// TODO: Better handling of premultiplied alpha
@@ -618,8 +618,8 @@
 			boxBlurT(imgA, imgB, w, h, Std.int(by), 3);
 		}
 
-		var imgB = image.data;
-		var imgA = sourceImage.data;
+		var imgB = sourceImage.data;
+		var imgA = destImage.data;
 		var w = Std.int (sourceRect.width);
 		var h = Std.int (sourceRect.height);
 		var bx = Std.int (blurX);
@@ -650,7 +650,7 @@
 			while (y < h) {
 				x = 0;
 				while (x < w) {
-					translatePixel(imgB, sourceImage.rect, image.rect, destPoint, x, y, strength);
+					translatePixel(imgA, sourceImage.rect, destImage.rect, destPoint, x, y, strength);
 					x += 1;
 				}
 				y += 1;
@@ -660,23 +660,23 @@
 			while (y >= 0 ) {
 				x = w-1;
 				while (x >= 0) {
-					translatePixel(imgB, sourceImage.rect, image.rect, destPoint, x, y, strength);
+					translatePixel(imgA, sourceImage.rect, destImage.rect, destPoint, x, y, strength);
 					x -= 1;
 				}
 				y -= 1;
 			}
 		}
 
-		image.dirty = true;
-		image.version++;
 		sourceImage.dirty = true;
 		sourceImage.version++;
-		
-		if (imagePremultiplied) image.premultiplied = true;
+		destImage.dirty = true;
+		destImage.version++;
+
 		if (sourceImagePremultiplied) sourceImage.premultiplied = true;
+		if (destImagePremultiplied) destImage.premultiplied = true;
 
-		if (imgB == image.data) return image;
-		return sourceImage;
+		if (imgA == sourceImage.data) return sourceImage;
+		return destImage;
 
 	}
 

and here is my sample to test - by clicking on the screen you can update .text property with Math.random()

package;

import openfl.filters.DropShadowFilter;
import openfl.events.MouseEvent;
import openfl.text.AntiAliasType;
import openfl.display.Bitmap;
import openfl.utils.Assets;
import openfl.text.TextFormatAlign;
import openfl.filters.BitmapFilterQuality;
import openfl.filters.GlowFilter;
import openfl.text.TextFormat;
import openfl.display.Sprite;
import openfl.text.TextField;

class Main extends Sprite {
    private var label_glow:TextField;
    private var label_base:TextField;

    public function new() {
        super();

        var dropShadow:DropShadowFilter = new DropShadowFilter(5.0, 45.0, 0x000000, 1.0, 0.0, 0.0, 150.0, BitmapFilterQuality.HIGH, false, false, false);
        var glow:GlowFilter = new GlowFilter(0x000000, 1.0, 2.0, 2.0, 150.0, BitmapFilterQuality.HIGH, false, false);

        var format:TextFormat = new TextFormat('Arial', 50, 0xFFFFFF, null, null, null, null, null, TextFormatAlign.CENTER, null, null, null, null);
        format.align = TextFormatAlign.CENTER;
        format.size = 100;
        format.color = 0x00FFFF;

        // base
        this.label_base = new TextField();
        this.label_base.defaultTextFormat = format;
        this.label_base.width = 700;
        this.label_base.height = 150;
        this.label_base.selectable = false;

        this.label_base.text = Std.string(Math.random());

        this.addChild(this.label_base);
        this.label_base.y = 0;
        this.label_base.filters = [ dropShadow ];

        // glow
        this.label_glow = new TextField();
        this.label_glow.defaultTextFormat = format;
        this.label_glow.width = 700;
        this.label_glow.height = 150;
        this.label_glow.selectable = false;
        this.label_glow.cacheAsBitmap = true;

        this.label_glow.text = Std.string(Math.random());

        this.addChild(this.label_glow);
        this.label_glow.y = 150;
        this.label_glow.filters = [ glow ];

        this.addEventListener(MouseEvent.CLICK, function(e:MouseEvent){
            this.label_base.text = Std.string(Math.random());
            this.label_glow.text = Std.string(Math.random());
        });
    }
}

Any workarounds to help with issues like these would be greatly appreciated:

A part of second issue (textfield not erased) will be resolved with this fix - it was my intent to fix this one as it was something I also need in our project. The first one is still valid and can be seen on my previous screenshot. I’ve updated my previous post with diffs.

EDIT: I’ve tested few things out - scaling seems still to be a problem but turning visibility off and on works OK. I can respond to the issues partially after you check my diffs and apply them.

EDIT2: Please comment on that clone() - is it OK performance-wise or is it unacceptable ?

Getting things to look right, I think, is first priority. I’m not sure if the clone() behaves properly with a multi-pass version of the filter, though? Perhaps we just need to reduce the number of passes, regardless?

Hmm, I’ve thought that in software rendering (__applyFilter) there is always one pass regardless of __numShaderPasses property setting ?

Yep, I guess you are right

OK. So to sum these up, I’ve not fixed how these filters look against FLASH (yet!) - this is something that would take me a little bit longer, as you said to propose a more solid/reliable gaussian blur for software targets but I’ve managed to fixed the following (software target only, hardware, shader-based are left untouched):

  • while filter applied, updating .text = 'Lorem Ipsum' property no longer stacks new value on top of the previous (@tour1st comment in here https://github.com/openfl/openfl/issues/2000) (in DropShadowFilter it also moved everything towards __offsetX/__offsetY every update)
  • drop shadow does no longer adjusts original text offset instead of adjusting shadow’s - it now properly adjusts shadow offset only (depends on __distance and __angle properties) leaving original text in the same position

Please reconsider pulling these in if you’ve checked them. It - at least - resolves some issues related to filters.

I’ve also have my own WIP on these three methods of glowing, outlining and softening edges - I saw multiple implementations and maybe someone will be interested in this :slight_smile: : https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf for hardware targets (but I’ve tried to get software targets working too - it’s still my local WIP though)

According to this stackoverflow article it is pretty hard to get flash-like blurs outside of flash but I guess I’ll try to get something closer-looking: https://stackoverflow.com/questions/32898588/what-blur-algorithm-does-flash-use-internally-in-their-blur-filter

Oh, I see your code here now:

It looks like it has the OpenFL diff twice, though. Could you share the change in Lime?

Thank you :slight_smile:

1 Like

Here you go. I’ve updated the lime section with a patch. I’ve switched places and renamed few things - the important thing in here was the translation which seemed to be on the right image but apparently it was not. Thanks a lot, I appreciate your time helping me out with this :slight_smile: