Artifacts when drawing background with dynamically generated 2d texture

Hi. I am trying to draw programatically generated texture using SkiaSharp using visual element.
Texture contains alpha channel.
I am setting texture as VisualElement background. Unfortunately I observing picture like this:
6214070--683027--upload_2020-8-17_18-14-24.png
As you can see, there are black outline on circle borders.
For example, exported image in Pain.NET looks like this:
6214070--683036--upload_2020-8-17_18-16-30.png
so SkiaSharp generates “clean” texture.

Any information I can provide to diagnose and fix this issue?

Hello,

There isn’t anything obvious from what you are describing. Can you maybe submit a bug in Unity with the code that produces this image or maybe share the exported texture here before it’s passed to the Visual Element, so we can try to identify where the issue comes from.

Code to draw animated circle I used:

using SkiaSharp;
using UnityEngine.UIElements.Experimental;

namespace UnityEngine.UIElements
{
    public class SkiaCanvas : VisualElement
    {
        public new class UxmlFactory : UxmlFactory<SkiaCanvas>
        {
        }

        SKImageInfo info;
        SKSurface surface;
        SKCanvas canvas;
        SKShader shader;
        SKPaint gradientPaint;
        Texture2D texture;
        TextureFormat format;
        ValueAnimation<float> animation;

        public SkiaCanvas()
        {
            info = new SKImageInfo(256, 256, SKColorType.RgbaF32);
            surface = SKSurface.Create(info);
            canvas = surface.Canvas;

            shader = SKShader.CreateSweepGradient(
                new SKPoint(info.Rect.MidX, info.Rect.MidY),
                new[] {SKColors.Gray, SKColors.WhiteSmoke},
                null
            );

            gradientPaint = new SKPaint
            {
                Shader = shader,
                StrokeWidth = 30,
                Style = SKPaintStyle.Stroke,
                IsAntialias = true
            };
            canvas.DrawCircle(128, 128, 98, gradientPaint);
            texture = new Texture2D(info.Width, info.Height, TextureFormat.RGBAFloat, false, true);
            style.backgroundImage = Background.FromTexture2D(texture);
          
            animation = experimental.animation
                .Start(0, Mathf.PI * 2, 1000, UpdateTexture)
                .KeepAlive()
                .Ease(Easing.Linear)
                .OnCompleted(() => { animation.Start(); });
        }

        void UpdateTexture(VisualElement thisElement, float angle)
        {
            canvas.Clear();
            canvas.ResetMatrix();
            canvas.SetMatrix(SKMatrix.CreateRotation(-angle, 128, 128));
            canvas.DrawCircle(128, 128, 98, gradientPaint);
            var pixmap = surface.PeekPixels();
            texture.LoadRawTextureData(pixmap.GetPixels(), pixmap.RowBytes * pixmap.Height);
            texture.Apply(false, false);
            MarkDirtyRepaint();
        }
    }
}

Also I uploaded skia libraries to easily drop them in project and execute example.
In case you are not okay downloading and executing some random dlls from random sources, link for nuget with SkiaSharp - NuGet Gallery | SkiaSharp 2.80.1.
My packages:

{
  "dependencies": {
    "com.unity.2d.sprite": "1.0.0",
    "com.unity.collab-proxy": "1.2.16",
    "com.unity.ide.rider": "1.1.4",
    "com.unity.ide.vscode": "1.1.4",
    "com.unity.test-framework": "1.1.9",
    "com.unity.textmeshpro": "2.0.1",
    "com.unity.timeline": "1.2.10",
    "com.unity.ugui": "1.0.0",
    "com.unity.ui.builder": "1.0.0-preview.3",
    "com.unity.ui.runtime": "0.0.4-preview",
    "com.unity.modules.ai": "1.0.0",
    "com.unity.modules.androidjni": "1.0.0",
    "com.unity.modules.animation": "1.0.0",
    "com.unity.modules.assetbundle": "1.0.0",
    "com.unity.modules.audio": "1.0.0",
    "com.unity.modules.cloth": "1.0.0",
    "com.unity.modules.director": "1.0.0",
    "com.unity.modules.imageconversion": "1.0.0",
    "com.unity.modules.imgui": "1.0.0",
    "com.unity.modules.jsonserialize": "1.0.0",
    "com.unity.modules.particlesystem": "1.0.0",
    "com.unity.modules.physics": "1.0.0",
    "com.unity.modules.physics2d": "1.0.0",
    "com.unity.modules.screencapture": "1.0.0",
    "com.unity.modules.terrain": "1.0.0",
    "com.unity.modules.terrainphysics": "1.0.0",
    "com.unity.modules.tilemap": "1.0.0",
    "com.unity.modules.ui": "1.0.0",
    "com.unity.modules.uielements": "1.0.0",
    "com.unity.modules.umbra": "1.0.0",
    "com.unity.modules.unityanalytics": "1.0.0",
    "com.unity.modules.unitywebrequest": "1.0.0",
    "com.unity.modules.unitywebrequestassetbundle": "1.0.0",
    "com.unity.modules.unitywebrequestaudio": "1.0.0",
    "com.unity.modules.unitywebrequesttexture": "1.0.0",
    "com.unity.modules.unitywebrequestwww": "1.0.0",
    "com.unity.modules.vehicles": "1.0.0",
    "com.unity.modules.video": "1.0.0",
    "com.unity.modules.vr": "1.0.0",
    "com.unity.modules.wind": "1.0.0",
    "com.unity.modules.xr": "1.0.0"
  }
}

6214952–683198–SkiaSharp.zip (3.48 MB)

Hi,

From my experience, theses “outline” artifact happens when the background color (even if the alpha is 0) is mixed with the side of the foreground during interpolation.

You can try to fill the background with a transparent red to see if the outline changes color.

Once confirmed, there are two ways to handle the issue:

  • You need to overdraw the color so that it fill a few pixels were the alpha =0
  • Or you make the image rendering pixel perfect, so guarantee that there is no interpolation.

The second one is usually harder to achieve, especially considering the different resolutions, scale factor and aspect ratio that the clients can have.

Hi. Thanks for reply.

So, looks like that is really and issue with interpolation. When I am trying to change background color, that outline changes color as well.

Disabling AA in Skia generates pixel-perfect image, obviously with pixel stairs, but no outline anymore.

And I don’t clearly understand what is the “You need to overdraw the color so that it fill a few pixels were the alpha =0”. Can you please explain a little bit more?

Great, at least we know where the issue is coming from. Basically, you don’t want an abrupt transition in alpha values at the same place as a transition in the color.

An example: (image take from and unrelated thread of stack overflow)

Because of the interpolation, color on the edge are blended. If one is (0, 0, 0, 1) and the other is (1,1,1,0), you may obtain (0.5, 0.5, 0.5, 0,5) or (0.25, 0.25, 0.25, 0.75) or a proportionally coherent value base on where the interpolation exactly occurs on the graphic card. Because of this, the white should have never been shown without interpolation, but you can see a bit of gray on the edge resulting from the blending.

1 Like

You can possibly draw the circle with a first SKPaint having a slightly larger brush and the color set to the same RGB, but a value of 0 alpha.

I personally never used the library, there is probably a way to achieve the same effect with a single SKPaint.

1 Like

Thanks a lot for reply. Will try your advices.
Also, its interesting that if I am saving image as PNG in memory and loading it to texture - there is no outline anymore:

            texture.LoadImage(surface.Snapshot().Encode(SKEncodedImageFormat.Png, 100).ToArray());

Not sure what magic happens behind.

1 Like

Hey, so digging with this alpha problem, I found one simple and fast solution - added SKAlphaType.Unpremul.

info = new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Unpremul);

Now alpha channel processed correctly and I have perfect shapes.

Thanks for help.

1 Like