Blur Background

How would you go about blurring the background in ui builder?

Filters aren’t yet supported out-of-the-box with UI Toolkit. That’s something that’s on the roadmap.

In the short term, if you feel adventurous, you can grab the background, blur it with a simple blur shader, and use it (or a portion of it) as the background of your root VisualElement.

Getting your hand on the background depends on the render pipeline that you use:

Hope this helps.

4 Likes

Has anyone gotten this to work? Getting the background into a RenderTexture isn’t hard, but its position will not be aligned with the UI element (it will be the whole screen).

Display the texture with an Image instead, and set the sourceRect value according to the position/size of your element.

1 Like

Thanks!!!

@AlexandreT-unity Sorry to ask, but I can’t seem to figure out how to get the (post-transformation) position and size in screen space. My attempt was this:

Rect worldBound = _container.worldBound;
Vector3 worldMin = new Vector3(worldBound.xMin, worldBound.yMin, 0);
Vector3 worldMax = new Vector3(worldBound.xMax, worldBound.yMax, 0);
Vector2 screenMin = _mainCamera.WorldToScreenPoint(worldMin);
Vector2 screenMax = _mainCamera.WorldToScreenPoint(worldMax);
Rect screenRect = Utils.absRect(screenMin, screenMax);
if(_container.sourceRect != screenRect)
    _container.sourceRect = screenRect;

But that seems like it’s about the same as before, if not worse.

Sorry for the confusion but VisualElement.worldBound doesn’t necessarily match the world observed by the Camera. It should have been named panelBound because it’s the value in panel space. Depending on the UI scaling/dpi and camera transformation, the panel space, world space, and screen space, may not match.

So basically what you need to do is to bring the screen rect into panel space. For that, convert the screen-space bottom/left and top/right corners of the screen with RuntimePanelUtils.ScreenToPanel. Then get the image element world rect (panel space rect). And finally convert these into absolute texture coordinates.

So it should somewhat look like this:

// Coordinates of the screen in panel space
var screenBLPanel = RuntimePanelUtils.ScreenToPanel(panel, new Vector2(0, 0));
var screenTRPanel = RuntimePanelUtils.ScreenToPanel(panel, new Vector(Screen.width, Screen.height));
var screenWPanel = screenTRPanel.x - screenBLPanel.x;
var screenHPanel = screenTRPanel.y - screenBLPanel.y;

// Coordinates of the VisualElement in panel space
var vePanel = image.worldBound

// Convert to texture coordinates
// Remember: panel space y axis increases downwards!
float left = Mathf.Abs((vePanel.xMin - screenBLPanel.x) / screenWPanel * textureWidth);
float right = Mathf.Abs((vePanel.xMax - screenBLPanel.x) / screenWPanel * textureWidth);
float bottom = Mathf.Abs((vePanel.yMax - screenTRPanel.y) / screenHPanel * textureHeight);
float top = Mathf.Abs((vePanel.yMin - screenTRPanel.y) / screenHPanel * textureHeight);

image.sourceRect = new Rect(left, bottom, right - left, top - bottom);
2 Likes

It’s still not working, but could it be because it is a RenderTexture?

I logged out all the results and see this:

screenBLPanel=(0.00, 0.00)
screenTRPanel=(1561.00, 817.00)
vePanel=(x:100.00, y:100.00, width:1361.00, height:617.00)
left=100
right=1461
top=717
bottom=100
sourceRect=(x:100.00, y:100.00, width:1361.00, height:617.00)
uv=(x:0.06, y:0.12, width:0.87, height:0.76)

Which all looks good; it’s even setting the UV rect to what I would assume is correct. But then…

7376234--899612--upload_2021-7-30_19-23-10.jpg

The yellow rect is the border of the image control (I added it to help visualize). So the image is somehow being scaled down and there’s a 100 pixel border on the INSIDE of the image rect (but only in the X axis) causing the display to be off. And it’s still using the whole screen (notice the door on the right) instead of the specified UV rect.

You need to set image.scaleMode to StretchToFill. I also noticed that the documentation of sourceRect mentions “The source rectangle inside the texture relative to the top left corner”. I wrote that code assuming that it was relative to the bottom left corner, so you might have to adjust the code for that (it may not be observable as long as you are vertically centered).

1 Like

Woah thanks for answering (on a Saturday morning no less; go get some weekend!) So it is wotrking, although the edges are not rounded, but I assume that’s just a limitation of the image control and I should hide it a different way:

7376489--899666--upload_2021-7-30_23-1-41.jpg

Hey if anyone in the future is searching for this, here’s how I’m doing the background image:

using System;
using burningmime.unity.graphics;
using UnityEngine;
using UnityEngine.UIElements;
using UDebug = UnityEngine.Debug;

namespace burningmime.unity.ui
{
    public class GlassyPanel : IDisposable
    {
        private readonly Image _image;
        private readonly IMenuBackground _background;
        private Vector2 _oldScreenSize;
        private Rect _oldPanelRect;
       
        public GlassyPanel(Image image)
        {
            _image = image;
            _image.scaleMode = ScaleMode.StretchToFill;
            _background = Inject.get<IMenuBackgroundService>().getBackground();
        }
       
        public void update()
        {
            RenderTexture texture = _background.texture;
            bool imageChanged = _image.image != _background.texture;
            if(imageChanged)
                _image.image = texture;
               
           
            if(texture != null)
            {
                Vector2 screenSize = new(Screen.width, Screen.height);
                Rect panelRect = _image.worldBound;
                bool refreshUVs = imageChanged || screenSize != _oldScreenSize || panelRect != _oldPanelRect;
                if(refreshUVs)
                {
                    _oldScreenSize = screenSize;
                    _oldPanelRect = panelRect;
                   
                    // Coordinates of the screen in panel space
                    IPanel panel = _image.panel;
                    var screenBLPanel = RuntimePanelUtils.ScreenToPanel(panel, new Vector2(0, 0));
                    var screenTRPanel = RuntimePanelUtils.ScreenToPanel(panel, screenSize);
                    var screenWPanel = Math.Abs(screenTRPanel.x - screenBLPanel.x);
                    var screenHPanel = Math.Abs(screenTRPanel.y - screenBLPanel.y);

                    // Convert to texture coordinates
                    // Remember: panel space y axis increases downwards!
                    float left = Mathf.Abs((panelRect.xMin - screenBLPanel.x) / screenWPanel * texture.width);
                    float right = Mathf.Abs((panelRect.xMax - screenBLPanel.x) / screenWPanel * texture.width);
                    float bottom = Mathf.Abs((panelRect.yMax - screenTRPanel.y) / screenHPanel * texture.height);
                    float top = Mathf.Abs((panelRect.yMin - screenTRPanel.y) / screenHPanel * texture.height);
                    _image.sourceRect = Utils.absRect(new Vector2(left, top), new Vector2(right, bottom));
                    _image.MarkDirtyRepaint();
                }
            }
        }

        public void Dispose()
        {
            _image.image = null;
            _background.Dispose();
        }
    }
}

And the background blur (using SRP):

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UDebug = UnityEngine.Debug;

namespace burningmime.unity.graphics
{
    [Singleton(SingletonScope.PROJECT)]
    class MenuBackgroundService : MonoBehaviour, IMenuBackgroundService
    {
        private const float RESIZE_DELAY = .2f;
        private RenderPassService _passService;
        private MenuBackgroundRenderPass _pass;
        private Material _material;
        private float _totalTime;
        private RenderTexture _target;
        private int _refCount;
        private Vector2Int _resizeTo;
        private Vector2Int _currentSize;
        private float _resizeTimer;

        private void Start()
        {
            enabled = false;
            _passService = Inject.get<RenderPassService>();
            _material = new Material(Inject.get<GraphicsResources>().UIBackgroundBlur);
            _material.SetFloat(Shader.PropertyToID("_blurriness"), 1);
            _pass = new MenuBackgroundRenderPass(this);
        }

        private void incremenetRefCount()
        {
            if(_refCount <= 0)
            {
                _refCount = 1;
                _passService.addPass(_pass);
                _target = new RenderTexture(Screen.width, Screen.height, 0);
                _currentSize = new Vector2Int(Screen.width, Screen.height);
                enabled = true;
            }
            else
            {
                ++_refCount;
            }
        }

        private void decrementRefCount()
        {
            if(_refCount > 1)
                --_refCount;
            else
                doDestroy();
        }

        private void Update()
        {
            if(_refCount <= 0)
            {
                doDestroy();
                return;
            }

            Vector2Int newSize = new(Screen.width, Screen.height);
            if(newSize != _currentSize)
            {
                if(newSize == _resizeTo)
                {
                    _resizeTimer += Time.unscaledDeltaTime;
                    if(_resizeTimer > RESIZE_DELAY)
                    {
                        _target.safeDestroy();
                        _target = new RenderTexture(newSize.x, newSize.y, 0);
                        _resizeTo = newSize;
                        _currentSize = newSize;
                        _resizeTimer = 0;
                    }
                }
                else
                {
                    _resizeTo = newSize;
                    _resizeTimer = 0;
                }
            }
        }

        private void OnDestroy() => doDestroy();
        private void doDestroy()
        {
            _refCount = 0;
            _passService.removePass(_pass);
            _target.safeDestroy();
            _target = null;
            enabled = false;
        }

        public IMenuBackground getBackground() => new MenuBackground(this);
        private class MenuBackground : IMenuBackground // not pooling it because it's not used oftene nough to warrant it and UI has a ton of allocations anyway
        {
            private readonly MenuBackgroundService _owner;
            private bool _isDisposed;
            public MenuBackground(MenuBackgroundService owner) { _owner = owner; owner.incremenetRefCount(); }
            public RenderTexture texture => _owner._target;
            public void Dispose() { if(!_isDisposed) _owner.decrementRefCount(); _isDisposed = true; }
        }

        private class MenuBackgroundRenderPass : SRPUtil.BaseRenderPass
        {
            private readonly MenuBackgroundService _owner;
            private RenderTargetHandle _rtIntermediate;

            public MenuBackgroundRenderPass(MenuBackgroundService owner) : base(RenderPassEvent.AfterRendering)
            {
                _owner = owner;
                _rtIntermediate.Init("_" + nameof(MenuBackgroundRenderPass) + "_Intermediate");
            }

            protected override void executeImpl(ScriptableRenderContext ctx, CommandBuffer cmd, ref RenderingData rd)
            {
                if(_owner._target != null)
                {
                    RenderTextureDescriptor intermediateDesc = describeIntermediateTarget(rd.cameraData.cameraTargetDescriptor);
                    cmd.GetTemporaryRT(_rtIntermediate.id, intermediateDesc, FilterMode.Bilinear);
                    Blit(cmd, rd.cameraData.renderer.cameraColorTarget, _rtIntermediate.Identifier(), _owner._material, 0);
                    Blit(cmd, _rtIntermediate.Identifier(), _owner._target, _owner._material, 1);
                    cmd.ReleaseTemporaryRT(_rtIntermediate.id);
                }
            }
          
            private static RenderTextureDescriptor describeIntermediateTarget(in RenderTextureDescriptor cameraRT)
            {
                RenderTextureDescriptor rt = cameraRT;
                rt.depthBufferBits = 0;
                rt.msaaSamples = 1;
                return rt;
            }
        }
    }
}

And the blur shader:

// Offset and weight numbers taken from: https://forum.unity3d.com/threads/simple-optimized-blur-shader.185327/#post-1267642
Shader "burningmime/UIBackgroundBlur"
{
    Properties
    {
        _blurriness ("Blurriness", Range(0, 30)) = 1
        [HideInInspector] _MainTex ("MainTex", 2D) = "white" {}
    }
  
    HLSLINCLUDE
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        struct a2v { float4 position : POSITION; float2 uv: TEXCOORD0; };
        struct v2f { float4 position : SV_POSITION; float2 uv : TEXCOORD0; };
  
        sampler2D _MainTex;
        float4 _MainTex_ST;
        float4 _MainTex_TexelSize;
        float _blurriness;
      
        static const float BLUR_DISTANCE_MULT = 4;
  
        v2f fullscreen_VS(a2v IN)
        {
            v2f OUT;
            OUT.position = TransformObjectToHClip(IN.position.xyz);
            OUT.uv = IN.uv;
            return OUT;
        }
  
        #define BLUR_SAMPLE_COORD(COORD, OFS) saturate(IN.uv.COORD + OFS * BLUR_DISTANCE_MULT * _MainTex_TexelSize.COORD * _blurriness)
        #define BLUR_SAMPLE_COORDS_X(OFS) float2(BLUR_SAMPLE_COORD(x, OFS), IN.uv.y)
        #define BLUR_SAMPLE_COORDS_Y(OFS) float2(IN.uv.x, BLUR_SAMPLE_COORD(y, OFS))
        #define BLUR_SAMPLE_ADD(IS_X, WEIGHT, OFS) sum += tex2D(_MainTex, (IS_X ? BLUR_SAMPLE_COORDS_X(OFS) : BLUR_SAMPLE_COORDS_Y(OFS))) * WEIGHT;
        #define BLUR_SAMPLE_FUNC(IS_X) \
            float4 sum = float4(0, 0, 0, 0); \
            BLUR_SAMPLE_ADD(IS_X, 0.05, -4.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.09, -3.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.12, -2.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.15, -1.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.18,  0.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.15, +1.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.12, +2.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.09, +3.0) \
            BLUR_SAMPLE_ADD(IS_X, 0.05, +4.0)
  
        float4 blurHoriz_PS(v2f IN) : SV_Target
        {
            BLUR_SAMPLE_FUNC(true)
            return float4(sum.xyz, 1);
        }
  
        float4 blurVert_PS(v2f IN) : SV_Target
        {
            BLUR_SAMPLE_FUNC(false)
            //float3 hsv = rgb2hsv(sum.xyz);
            //hsv.y = saturate(hsv.y - DESATURATE_SUB * _blurriness) * lerp(1, DESATURATE_MULT, _blurriness);
            //return float4(hsv2rgb(hsv), 1);
            return float4(sum.xyz, 1);
        }
    ENDHLSL

    SubShader
    {
        Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
      
        Pass
        {
            Tags { "LightMode" = "SRPDefaultUnlit" }
            ZTest Always
            ZWrite Off
            Cull Off
            HLSLPROGRAM
                #pragma vertex fullscreen_VS
                #pragma fragment blurHoriz_PS
            ENDHLSL
        }
      
        Pass
        {        
            Tags { "LightMode" = "SRPDefaultUnlit" }
            ZTest Always
            ZWrite Off
            Cull Off
            HLSLPROGRAM
                #pragma vertex fullscreen_VS
                #pragma fragment blurVert_PS
            ENDHLSL
        }
    }
}

It has a few references to internal utility classes, but it should give the general idea.

11 Likes

You’re welcome, glad you got it to work :slight_smile:

With the Image element, the image is drawn as a foreground, like text. If you want rounded corners that will clip the image, set overflow to hidden and set border-bottom/top-left/right-radius > 0.

1 Like

I tried to create a custom VisualElement with Uxml attributes for blur parameters (so it would be controllable from uxml and builder) but I’m not getting any meaningful result.
Would anybody be able to tell me what’s wrong with my code?

using UnityEngine;
using UnityEngine.UIElements;

namespace Test
{
    public class BlurImage : VisualElement
    {
        private readonly Material _blurMaterial = new(Shader.Find("Custom/Blur"));

        private Texture _srcTexture;
        private RenderTexture _destTexture;

        public int Radius
        {
            get => _blurMaterial.GetInt("_Radius");
            set
            {
                _blurMaterial.SetInt("_Radius", value);
                BlurTexture();
            }
        }

        public BlurImage()
        {

            RegisterCallback<GeometryChangedEvent>(evt =>
            {
                BlurTexture();
            });

            RegisterCallback<AttachToPanelEvent>(evt =>
            {
                BlurTexture();
            });
        }

        private void BlurTexture()
        {
            if (_srcTexture == null)
                _srcTexture = resolvedStyle.backgroundImage.texture ?? resolvedStyle.backgroundImage.sprite?.texture;

            if (_srcTexture == null)
                return;


            style.backgroundImage = ToTexture2D();
            this.MarkDirtyRepaint();
        }

        private Texture2D ToTexture2D()
        {

            RenderTexture currentRT = RenderTexture.active;

            var renderTexture = new RenderTexture(_srcTexture.width, _srcTexture.height, 32);
            Graphics.Blit(_srcTexture, renderTexture, _blurMaterial);

            RenderTexture.active = renderTexture;
            var texture2D = new Texture2D(_srcTexture.width, _srcTexture.height, TextureFormat.RGBA32, false);
            texture2D.ReadPixels(new Rect(0, 0, renderTexture.width, renderTexture.height), 0, 0);
            texture2D.Apply();

            RenderTexture.active = currentRT;

            return texture2D;
        }

        public new class UxmlFactory : UxmlFactory<BlurImage, UxmlTraits> { }
        public new class UxmlTraits : VisualElement.UxmlTraits
        {
            private readonly UxmlIntAttributeDescription _radius = new()
            {
                name = "radius",
                defaultValue = 10
            };

            public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                var image = ve as BlurImage;

                image.Radius = _radius.GetValueFromBag(bag, cc);
            }
        }
    }
}

The shader:

Shader "Custom/Blur"
{
    Properties
    {
        _Radius("Radius", Range(1, 255)) = 1
    }

    Category
    {
        Tags{ "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Opaque" }

        SubShader
        {
            GrabPass
            {
                Tags{ "LightMode" = "Always" }
            }

            Pass
            {
                Tags{ "LightMode" = "Always" }

                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma fragmentoption ARB_precision_hint_fastest
                #include "UnityCG.cginc"

                struct appdata_t
                {
                    float4 vertex : POSITION;
                    float2 texcoord: TEXCOORD0;
                };

                struct v2f
                {
                    float4 vertex : POSITION;
                    float4 uvgrab : TEXCOORD0;
                };

                v2f vert(appdata_t v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    #if UNITY_UV_STARTS_AT_TOP
                    float scale = -1.0;
                    #else
                    float scale = 1.0;
                    #endif
                    o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
                    o.uvgrab.zw = o.vertex.zw;
                    return o;
                }

                sampler2D _GrabTexture;
                float4 _GrabTexture_TexelSize;
                float _Radius;

                half4 frag(v2f i) : COLOR
                {
                    half4 sum = half4(0,0,0,0);

                    #define GRABXYPIXEL(kernelx, kernely) tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(float4(i.uvgrab.x + _GrabTexture_TexelSize.x * kernelx, i.uvgrab.y + _GrabTexture_TexelSize.y * kernely, i.uvgrab.z, i.uvgrab.w)))

                    sum += GRABXYPIXEL(0.0, 0.0);
                    int measurments = 1;

                    for (float range = 0.1f; range <= _Radius; range += 0.1f)
                    {
                        sum += GRABXYPIXEL(range, range);
                        sum += GRABXYPIXEL(range, -range);
                        sum += GRABXYPIXEL(-range, range);
                        sum += GRABXYPIXEL(-range, -range);
                        measurments += 4;
                    }

                    return sum / measurments;
                }
                ENDCG
            }
            GrabPass
            {
                Tags{ "LightMode" = "Always" }
            }

            Pass
            {
                Tags{ "LightMode" = "Always" }

                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
                #pragma fragmentoption ARB_precision_hint_fastest
                #include "UnityCG.cginc"

                struct appdata_t
                {
                    float4 vertex : POSITION;
                    float2 texcoord: TEXCOORD0;
                };

                struct v2f
                {
                    float4 vertex : POSITION;
                    float4 uvgrab : TEXCOORD0;
                };

                v2f vert(appdata_t v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    #if UNITY_UV_STARTS_AT_TOP
                    float scale = -1.0;
                    #else
                    float scale = 1.0;
                    #endif
                    o.uvgrab.xy = (float2(o.vertex.x, o.vertex.y*scale) + o.vertex.w) * 0.5;
                    o.uvgrab.zw = o.vertex.zw;
                    return o;
                }

                sampler2D _GrabTexture;
                float4 _GrabTexture_TexelSize;
                float _Radius;

                half4 frag(v2f i) : COLOR
                {

                    half4 sum = half4(0,0,0,0);
                    float radius = 1.41421356237 * _Radius;

                    #define GRABXYPIXEL(kernelx, kernely) tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(float4(i.uvgrab.x + _GrabTexture_TexelSize.x * kernelx, i.uvgrab.y + _GrabTexture_TexelSize.y * kernely, i.uvgrab.z, i.uvgrab.w)))

                    sum += GRABXYPIXEL(0.0, 0.0);
                    int measurments = 1;

                    for (float range = 1.41421356237f; range <= radius * 1.41; range += 1.41421356237f)
                    {
                        sum += GRABXYPIXEL(range, 0);
                        sum += GRABXYPIXEL(-range, 0);
                        sum += GRABXYPIXEL(0, range);
                        sum += GRABXYPIXEL(0, -range);
                        measurments += 4;
                    }

                    return sum / measurments;
                }
                ENDCG
            }
        }
    }
}
1 Like

did you get any solution for this?

Unfortunately, no

Anything new in this realm? :slight_smile:

Shameless self-plug*:

I have made an asset for that. It supports Built-In, URP and HDRP and also takes care of all the borders, padding and round corners.

If anyone who has posted in this thread before is interested in a free voucher, PM me. It’s fairly new and I am always looking for feedback :slight_smile:

Links: Forum Thread , Asset Store, Manual

  • @ : If promoting the asset here is not okay please just delete my post. I stumbled upon this while googling and thought it might be a nice addition to the thread. Thank you.
1 Like

Hi @mcoted3d , you said that it was on the roadmap? Is it supported now?

It would be nice to have the ability to blur as easily as in CSS

.blurry-div {
  filter: blur(10px);
}

This is still on the roadmap, but not supported right now. We are currently reworking the render backend to support that.

1 Like

4 months later, any update on this? Custom shaders would unlock this obviously so it should really be a high priority to get done on the UI Toolkit! Thanks

1 Like