Performance of an animated mask

I’m pretty trash at graphics performance, and hoping to get some advice.

I’ve implemented a circular wipe between two views (full-screen Sprites). It works by masking the new view with a circular Shape, placing it on top of the old view, then tweening the mask’s scaleX and scaleY until it fills the whole screen.

It works perfectly in html5, but on native Android the framerate plummets when the mask nears the size of the screen. Oddly, the same animation in html5 running on the same Android device is silky smooth. Any ideas would be much appreciated.

Here’s the relevant code:

addChild(background);
addChild(foreground);

var mask = new Shape();
mask.graphics.beginFill(0xffffff);
mask.graphics.drawCircle(0, 0, SCREEN_HEIGHT);
mask.graphics.endFill();
mask.x = WIPE_CENTER_X;
mask.y = WIPE_CENTER_Y;
mask.scaleX = mask.scaleY = 0;
parent.addChild(mask);
foreground.mask = mask;

Actuate
    .tween(foreground.mask, Constants.WIPE_DURATION, { scaleX: 1, scaleY: 1 })
   .ease(Quad.easeOut)
   .onComplete(() -> {
       foreground.mask = null;
       parent.removeChild(mask);
   });

Try fiddling with the cacheAsBitmap flag or using a bitmapdata (an image) instead of drawing the circle

No dice. Tried various combinations of cacheAsBitmap on foreground, background, and mask and nothing improved performance.

I also tried var mask = new Bitmap(Assets.getBitmapData('assets/mask.png'));, but it doesn’t appear to be respecting the alpha channel, and so I just get a square mask.

I’m starting to investigate using a ShaderFilter, but setting foreground.filters = [ new ShaderFilter(maskShader) ] (where maskShader is a new Shader()) causes an exception to be thrown on render, and I don’t understand why.

Does -Dopenfl-disable-graphics-upscaling help? (like <define name="openfl-disable-graphics-upscaling" /> in your project.xml)

Weeeird. That breaks my animation entirely. Instead of scaling, the circle goes zooming off toward the top-left. What does that flag do?

Hah, I guess it might have been broken. It is supposed to tell OpenFL to not upscale graphics – changing scaleX or scaleY causes OpenFL to (by default) redraw the shape. Perhaps cacheAsBitmap + cacheAsBitmapMatrix?

I added the following lines, just before triggering the Actuate tween:

mask.cacheAsBitmapMatrix = mask.transform.concatenatedMatrix;
mask.cacheAsBitmap = true;

There’s no discernible effect on performance. =(

More strange behaviour:
When I use cacheAsBitmapMatrix, I can’t seem to translate the circle. It renders at (0,0) in its parent container, whether I set the x/y properties or apply a matrix that includes translations. Not sure if this is related to the odd translation behaviour I see when using openfl-disable-graphics-upscaling but I figure it’s worth a mention.

Did some investigation and logged a bug here.

Still fighting with this, months later =(
With the openfl-disable-graphics-upscaling bug still open, I feel like using a pre-generated Bitmap as a mask is the most promising direction. Does anyone know why the alpha channel isn’t being respected? Is there a workaround that I’m missing?

Thanks.

What version are you using? Does OpenFL 8.9.6 help?

I’m on 8.9.6, and behaviour is as described earlier.

Is there a way you can fake a mask by using an opaque bitmap with a transparent cut-out in the center?

Here’s what I’m guessing:

1.) Using mask with the mask.graphics results in the shape being redrawn as it scales. This is increasingly expensive to redraw based on size.
2.) Using a pre-made bitmap (or just bitmapData.draw) then scaling that for a mask does not respect the alpha on the bitmap so it masks only a rectangle

There’s TODO work in the development branch to support hardware shapes again (which solves the first issue with the noted drawback of more aliased edges) or I had an initial working version of respecting the alpha in a mask so long as both the mask and “maskee” are both bitmaps. I might be able to bring this to the 8.9 branch if we need it… or getting a mask to work with cacheAsBitmapMatrix might also be an idea workaround. I’m going to take a look at that

Oh I see yes in the renderGLMask code it ignores cacheAsBitmap as using a cached bitmap results in a square mask rather than the circle. Perhaps we need to handle this using a custom shader along with a pre-rendered bitmap

Can custom shaders have variables updated dynamically? If so, it should be reasonably simple to make a shader that sets opacity based on a radius and animate the radius.

Edit: They totally can! Though I’m at a loss at to how to force a Bitmap to update after its filter is changed. Just modifying the filter attribute doesn’t work, but if I first remove the filters (bitmap.filters = []) and then re-apply later (using Timer.delay), it updates. There’s got to be a better way, right? I would expect the invalidate method to help here, but it doesn’t…

Hmm, I thought that invalidate would do it. Does it work if you invalidate its parent?

Nope. I tried calling invalidate on the bitmap, its parent, and the stage. None work.
Changing the size of the bitmap does cause it to re-render and recalculate the filter, though. Is something in invalidate broken?

Is anything cacheAsBitmap here? If it’s cached perhaps it is not redrawing the cached image on invalidate though changing the size certainly does

Applying filters automatically enables cacheAsBitmap, no?

The cacheAsBitmap property is automatically set to true whenever you apply a filter to a display object

1 Like

Oh I thought you were using displayObject.shader not ShaderFilter