Simple Optimized Blur Shader

Hi,

I have a vertex-fragment shader that use the Unity3D GrabPass functionality (it grabs the screen). And I apply my GrabPass to have a transparent effect.

 GrabPass {
    	"_GrabTex"
    }
    
    sampler2D	_GrabTex;
    
    // float4 grabPassPos : TEXCOORD4 is used to apply the grabbed texture
    // in the right place and is calculated in the vertex
    half4 transparent = tex2Dproj(_GrabTex, UNITY_PROJ_COORD(IN.grabPassPos));
    half4 baseColor = transparent;
    return baseColor;

I am searching a simple way of blurring my grabbed texture. I don’t know how to apply my blur because it’s a tex2Dproj and I don’t know where to apply it.
I am searching for a one pass optimized simple blur but I don’t know if it’s possible.
I know a bit about Gaussian and Box blur which seems to be the simplest but how can I apply them in a tex2Dproj ?

I know this package but it seems to be controversial and not very optimized.

Thanks a lot !

Dont use grabpass, use a custom rendertexture and the simplest/lousiest way to blur it is to reduce the size of the texture after rendering to it.
Or there are also alternative ways as you mentioned.
The main point is, if you want control on things, then dont use grabpass.

Thanks for your answer Aubergine !
It is for a water transparency effect how can I use something other than GrabPass ? For me GrabPass is more optimized than a camera with a renderTexture but maybe I am wrong.
I can’t apply some blur on the grabbed texture ?

grabpass is a render texture itself that is rendered internally. You can do the blur with grabpass too, but if you want more control, render it yourself.

For the moment I want to apply blur on my grabpass texture.
I saw that I can do it like this :

float3 proj = UNITY_PROJ_COORD(grabWithOffset);
half4 top = tex2Dproj(_GrabTex, proj + float3(0, -_Blur, 0));
half4 bot = tex2Dproj(_GrabTex, proj + float3(0, _Blur, 0));
half4 left = tex2Dproj(_GrabTex, proj + float3(-_Blur, 0, 0));
half4 right = tex2Dproj(_GrabTex, proj + float3(_Blur,0, 0));
half4 center = tex2Dproj(_GrabTex, proj );			
return ((top + bot + left + right + center + center) / 6.0f);

But this is hardcoded. And the result is a very simple blur not smoothed.

I know that there is a concept of kernel but I don’t understand it. If you set the value in the kernel which seems to be an array how can you apply it in your texture.
And I saw that this kernel can be used in a loop, but in unity shader there is no access to for or while loops.

1 Like

Grabpass is different from a rendertexture in that it doesn’t require to re-render the scene. It blits the contents of the framebuffer to a temporary texture, which isn’t free but still much cheaper than rendering the scene multiple times.
Sure, you have more control with render to texture approaches, but for simple stuff grabpass should be fine.

I slapped toghether the refraction shader with gaussian blur. It’s a bit hackish, but gets the job done.

The kernel is hard coded for efficiency. If you know an efficient way for a dynamic kernel, let me know :wink:
Depending on geometry it could be more efficient to do a single pass with two dimensional gaussian, at least with a small kernel. Not sure though. I did it splitted in horizontal+vertical as everyone else and I’m too lazy to try.
Third pass is just for distortion and tint color.

Shader "Custom/SimpleGrabPassBlur" {
	Properties {
		_Color ("Main Color", Color) = (1,1,1,1)
		_BumpAmt  ("Distortion", Range (0,128)) = 10
		_MainTex ("Tint Color (RGB)", 2D) = "white" {}
		_BumpMap ("Normalmap", 2D) = "bump" {}
		_Size ("Size", Range(0, 20)) = 1
	}
	
	Category {
	
		// We must be transparent, so other objects are drawn before this one.
		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Opaque" }
	
	
		SubShader {
		
			// Horizontal blur
			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 = mul(UNITY_MATRIX_MVP, 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 _Size;
				
				half4 frag( v2f i ) : COLOR {
//					half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
//					return col;
					
					half4 sum = half4(0,0,0,0);

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

					sum += GRABPIXEL(0.05, -4.0);
					sum += GRABPIXEL(0.09, -3.0);
					sum += GRABPIXEL(0.12, -2.0);
					sum += GRABPIXEL(0.15, -1.0);
				    sum += GRABPIXEL(0.18,  0.0);
					sum += GRABPIXEL(0.15, +1.0);
					sum += GRABPIXEL(0.12, +2.0);
					sum += GRABPIXEL(0.09, +3.0);
					sum += GRABPIXEL(0.05, +4.0);
				    
					return sum;
				}
				ENDCG
			}

			// Vertical blur
			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 = mul(UNITY_MATRIX_MVP, 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 _Size;
				
				half4 frag( v2f i ) : COLOR {
//					half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
//					return col;
					
					half4 sum = half4(0,0,0,0);

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

					//G(X) = (1/(sqrt(2*PI*deviation*deviation))) * exp(-(x*x / (2*deviation*deviation)))
                    
					sum += GRABPIXEL(0.05, -4.0);
					sum += GRABPIXEL(0.09, -3.0);
					sum += GRABPIXEL(0.12, -2.0);
					sum += GRABPIXEL(0.15, -1.0);
				    sum += GRABPIXEL(0.18,  0.0);
					sum += GRABPIXEL(0.15, +1.0);
					sum += GRABPIXEL(0.12, +2.0);
					sum += GRABPIXEL(0.09, +3.0);
					sum += GRABPIXEL(0.05, +4.0);
				    
					return sum;
				}
				ENDCG
			}
	 		
	 		// Distortion
			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;
					float2 uvbump : TEXCOORD1;
					float2 uvmain : TEXCOORD2;
				};
				
				float _BumpAmt;
				float4 _BumpMap_ST;
				float4 _MainTex_ST;
				
				v2f vert (appdata_t v) {
					v2f o;
					o.vertex = mul(UNITY_MATRIX_MVP, 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;
					o.uvbump = TRANSFORM_TEX( v.texcoord, _BumpMap );
					o.uvmain = TRANSFORM_TEX( v.texcoord, _MainTex );
					return o;
				}
				
				fixed4 _Color;
				sampler2D _GrabTexture;
				float4 _GrabTexture_TexelSize;
				sampler2D _BumpMap;
				sampler2D _MainTex;
				
				half4 frag( v2f i ) : COLOR {
					// calculate perturbed coordinates
					half2 bump = UnpackNormal(tex2D( _BumpMap, i.uvbump )).rg; // we could optimize this by just reading the x  y without reconstructing the Z
					float2 offset = bump * _BumpAmt * _GrabTexture_TexelSize.xy;
					i.uvgrab.xy = offset * i.uvgrab.z + i.uvgrab.xy;
					
					half4 col = tex2Dproj( _GrabTexture, UNITY_PROJ_COORD(i.uvgrab));
					half4 tint = tex2D( _MainTex, i.uvmain ) * _Color;
					
					return col * tint;
				}
				ENDCG
			}
		}
	}
}

Here’s how it looks.
1267642--56175--$blur.jpg

Cheers.

10 Likes

Thanks a lot Cician ! I will look at your shader.

I’ve found an interesting optimization of gaussian blur.
But I don’t know if it’s really convincing.

I don’t understand why the number of pass should depend on the geometry.
Why do separate the refraction pass ? I thought that the more pass you have the less it’s optimized.
What are the interest of having multiple pass (I know that it’s of topic but I am just asking)

A strange but normal behaviour appended on zoom, I can see the duplicate of the texture, is there a way to avoid that ?

The shader in fact is not optimized at all.

Most people optimize gaussian blur by splitting it into horizontal and vertical pass, but in this case mesh geometry is rendered for each pass, which may outweight the benefits depending on multiple factors, such as triangle count and kernel size.
Its’s first time I’m doing the blur thingy and I’m just guessing this is the case, so take with a grain of salt.

As for diffraction pass, it’s just simpler this way. In fact the second and third passes can be combined into one.

I don’t understand. Screenshot or it didn’t happen.
Just a guess: try Z-Priming.

NB: usual gotchas apply…
Transparency issues: popping and no shadows.
Grabpass: foreground objects bleeding in the distortion (avoidable with a mask, see gpu gems) and artifacts near screen edges.

Here is a screenshot of what I think it’s strange but “normal”. When I zoom with the camera.

Where does it come from ? For me it depends of the kernel size and the smooth of the blur.

Is there a way to avoid this effect ?
Is there a solution to have an efficient simple blur effect. It seems that every blur is based on the duplication of the image then blending.

That’s exactly how blur is done cheaply. by taking a picture, blowing it up and showing it again.
More extensive forms of blur would become much more heavy (And i wouldn’t be sure how that’s computed either).

I can’t seem to get that example to work, throws a ton of errors. something about the shader not having certain properties, while those properties certainly seem to be there. I am still very much learning how shaders work, so it all seems very complex to me. I was having a look at the outline toon shader in unity to make something like a glow shader, but I can’t seem to figure out how I’d blur the outline =/
Maybe that shader is not the best thing to use as a base for it.

seems not work on mobile

This doesn’t work well on mobile but I managed to get rid of the artifacts up to ~30 pixel strength. The edges will suffer on that level of strength though.

Gaussian Blur

This is a 10 pixel (gap) Gaussian blur.

Just dropping a note. Food for thaught.
With command buffers one could avoid some costly memory copies of grab pass. At least between two passes of separable blur. See the first example here:

Hi Cician,
Have you updated your shader using the above approach Unity’s new CB. Please post your new shader here, if So…

No. Sorry, but I’m currently mostly working in Unreal Engine. I did try the provided example though. It works (albeit not correctly in editor viewport, correctly at runtime) and it uses a separable blur. Didn’t try anything beyond that.

Yup, I tried this too. But, It is not performance friendly… :(:frowning:
Any way, Thank you Cician for sharing the shader here, I really appreciate that… :roll_eyes::roll_eyes:

Reviving this thread as I’m trying to do small blurs like @flyingbanana using the Command Buffers approach example Aras posted. Unfortunately even on the latest iOS devices and rendering a small portion of the screen the framerate plummets.

Has anyone had success during these sorts of blurs with Command Buffers on mobile?

I think deferred would be your problem on mobile long before command buffers got involved… then there’s grabpass and all sorts of expensive stuff.