Radial Blur Shader - Texture

Hi everyone!

I’m working on a fake motion blur solution for the common “wagon wheel” problem in Unity 5.
At the beginning I wanted to create a local radial motion blur as Image Effect but it’s too hard for me, so I’m trying different approaches.

As now, I just need an shader to perform a radial blur on a texture using a float value (angle) as input, but I’m quite noob on shaders.

Here is an example of what I would like to reproduce:

Does anyone have some suggestions? I’d really appreciate some help! :slight_smile:

If you want to try using the 5.4 betas you can use this:

However doing this as an effect on the texture itself can be quite expensive. It requires you sample the texture multiple times in your shader. You can do some hacks like lowering the mip map to reduce the number of texture samples, but it will also blur details that wouldn’t blur normally. The really cheap method of doing this is have two textures, one blurred and one not, and fade between them. This is what a lot of racing games of the PS2/Xbox through PS3/Xbox 360 eras did, and even what the original Halo games did for the warthog.

2 Likes

or just some blur shader on that texture, but the fade one sounds better though…
Radial Blur Shader « Unity Coding – Unity3D *not exactly same effect

1 Like

That blur shader works by sampling the texture 100 times … which is very expensive. However the idea is the same for a spin blur, just rotating the texture each time instead of scaling. Something like 8 texture samples is okay, even 16 isn’t totally crazy, but that’s a total count so if you’ve got normal maps, and metallic / smoothness maps, and occlusion maps you can only realistically do 4 samples of each texture which doesn’t produce a very pleasing blur (also a normal map blurred this way produces kind of bad results).

2 Likes

Hi! I have some news.
After a lot of fails, and with your good suggestions I finally have something working!!!

This shader is based on the “FakeRadialBlur” shader made by @mgear , including the UV rotation function by @bgolus

Shader

Shader "Custom/SpinBlur"{
    Properties{
        tDiffuse("Color (RGB) Alpha (A)", 2D) = "white"
        angle ("Angle", Int) = 1
    }
    SubShader {
        Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

        LOD 200
        Cull Off
            CGPROGRAM
            #pragma target 3.0
            #pragma surface surf Lambert alpha
            sampler2D tDiffuse;
            int angle;
            struct Input {
            float2 uvtDiffuse;
            float4 screenPos;
            };

            float2 rotateUV(float2 uv, float degrees) {
                const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;
                float rotationRadians = degrees * Deg2Rad;
                float s = sin(rotationRadians);
                float c = cos(rotationRadians);
                float2x2 rotationMatrix = float2x2(c, -s, s, c);
                uv -= 0.5;
                uv = mul(rotationMatrix, uv);
                uv += 0.5;
                return uv;
            }

            void surf (Input IN, inout SurfaceOutput o){
                const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;
                const float Rad2Deg = 180.0 / UNITY_PI;
                float2 vUv = IN.uvtDiffuse;
                float2 coord = vUv;
                float illuminationDecay = 1.0;
                float4 FragColor = float4(0.0, 0.0, 0.0, 0.0);
                int samp = angle;
                if (samp <= 0) samp = 1;
                for(float i=0; i < samp; i++){      
                    coord = rotateUV(coord, angle/samp);
                    float4 texel = tex2D(tDiffuse, coord);
                    texel *= illuminationDecay * 1 / samp;
                    FragColor += texel;
                }
                float4 c = FragColor;
                o.Albedo = c.rgb;
                o.Alpha = c.a;
            }
            ENDCG
        }
    FallBack "Diffuse"
}

And that’s what I get for different values of the angle.
This is not cheap at all, because I use one sample for each degree of the angle.
For example, in the last image (360°) I use 360 samples, which is so expensive.

Maybe I should make it tweakable with some kind of downsampling.

And here’s some videos:

Thanks, guys!!! :slight_smile: :slight_smile: :slight_smile:

2 Likes

Hi!! I need your help again. I modified the shader so now it supports downsampling and some other optimizations.
I’m trying to make an Image Effect with it, and I know how to do it (using the Blit function), but it seems that Blit doesn’t work with surface shaders, producing black textures.

So, how can I convert this surface shader in a fragment shader with ColorMask RGBA?

Shader "Custom/SpinBlur"{
    Properties{
        _Color("Main Color", Color) = (1,1,1,1)
        _Samples("Samples", Range(0,360)) = 100
        _Angle("Angle", Range(0,360)) = 10
        _MainTex("Color (RGB) Alpha (A)", 2D) = "white"
    }
    SubShader{
    Tags{ "Queue" = "Transparent" "RenderType" = "Transparent" }

    LOD 200
    Cull Off

    CGPROGRAM
    #pragma target 3.0
    #pragma surface surf Lambert alpha

    sampler2D _MainTex;
    int _Angle;
    int _Samples;
    float4 _Color;
   
    struct Input {
        float2 uv_MainTex;
        float4 screenPos;
    };

    float2 rotateUV(float2 uv, float degrees) {
        const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;
        float rotationRadians = degrees * Deg2Rad;
        float s = sin(rotationRadians);
        float c = cos(rotationRadians);
        float2x2 rotationMatrix = float2x2(c, -s, s, c);
        uv -= 0.5;
        uv = mul(rotationMatrix, uv);
        uv += 0.5;
        return uv;
    }

    void surf(Input IN, inout SurfaceOutput o) {
        const float Deg2Rad = (UNITY_PI * 2.0) / 360.0;
        const float Rad2Deg = 180.0 / UNITY_PI;
        float2 vUv = IN.uv_MainTex;
        float2 coord = vUv;
        float4 FragColor = float4(0.0, 0.0, 0.0, 0.0);
        int samp = _Samples;
        if (samp <= 0) samp = 1;
        for (float i = 0; i < samp; i++) {
            float a = (float)_Angle / (float)samp;
            coord = rotateUV(coord, a);
            float4 texel = tex2D(_MainTex, coord);
            texel *= 1.0 / samp;
            FragColor += texel;
        }
        float4 c = FragColor*_Color;
        o.Albedo = c.rgb;
        o.Alpha = c.a;
    }
    ENDCG
    }
        FallBack "Diffuse"
}

Thanks again!

4 Likes

Thanks for sharing it works perfect.

1 Like

For a blit shader you want as basic of a vertex / fragment shader as possible. Create a new unlit shader in Unity and you can mostly copy your surf function into that then fix the issues. Main things are you’re directly outputting a fixed4 instead of o.Albedo and o.Alpha, and the “IN.uv_MainTex” is going to be “i.uv” or something like that.

1 Like

Thanks, @bgolus , that worked!! :slight_smile:

Thanks for sharing it works perfect.

1 Like

So basically I’ve been working on a HDRP project and I had to convert the code to Shader Graph but instead of using nodes I put the code inside a Custom Function Node

All you need is add this code with the following inputs and outputs

Inputs:
MainTex: Texture2D
UV: Vector2
SS: Sampler State
Angle: Vector1

Outputs:
Out: Vector4

const float Deg2Rad = 0.017453293;
const float Rad2Deg = 57.295777937;
float2 coord = UV;
float4 FragColor = float4(0.0, 0.0, 0.0, 0.0);
uint samp = Angle;

if (samp < 1)
        samp = 1;

for (float i = 0; i < samp; i++)
{
        float rotationRadians = i * Deg2Rad / samp;
        float s = sin(rotationRadians);
        float c = cos(rotationRadians);
        float2x2 rotationMatrix = float2x2(c, -s, s, c);

        coord -= 0.5;
        coord = mul(rotationMatrix, coord);
        coord += 0.5;

        float4 texel = SAMPLE_TEXTURE2D(MainTex, SS, coord);

        texel *= 1.0 / samp;
        FragColor += texel;
}

Out = FragColor;

6486146--728975--Screenshot_1.png

1 Like

Thank you for this contribution!

Just a performance consideration for both the text and the graph version of this shader: I wouldn’t use it for realtime rendering but only for baking a radially blurred pool/array/spritesheet of textures.

Francesco

that’s what I am exactly doing! Baking textures and saving them using a RenderTexture buffer along with Graphics.Blit to a Texture2D

just adding to original topic, new spin blur asset is coming:

and theres free version

3 Likes

Very interesting technique, basically it blurs the meshes by sampling them multiple times with lowered alpha, exactly the same way we blur textures. I’ll definitely give it a try.

1 Like

isn’t there an open source version that using the same tech as this?