Hello, I'm using a render texture to generate a depth map that displaces geometry. While the current result is passable, I think I can do better by blurring the depth map. The issue I'm having is that I just don't know how to go about blurring it. I've looked into Gaussian and Poisson Blurs, but the examples I've found for them are hard to understand and difficult to integrate into my existing surface shader. Does anyone have any thoughts, tips, or ideas? Thanks.
You want to do the blur to the render texture as blits, not as part of the surface texture. I mean you could do a basic poisson disc blur in the surface shader, but it’ll be far faster and much higher quality to do a separable Gaussian in two passes, or maybe a Kawase blur.
Ah okay. So I'll want to do the blur as a C# script then. Do you have any good resources on how to implement a Gaussian blur? Also, my interest in doing a Poisson disc blur was from this GDC presentation which tackles the same visual effect I'm working on.
That's from the Arkham Origins talk, yes? For a small blur, a poisson blur is fine. It's all about trying to reduce the number of taps (texture samples) being done. A separable gaussian can do a much wider blur for a sqrt() number of taps vs poisson for the same quality, but requires two passes. Poisson can be done in one pass, and if you're only doing 4 taps there's not a lot of reason to go with multiple passes for that. Note, the above slide is talking about doing the blur in the equivalent of a script side blit() as well. Also note, "poisson" really just means random points with a minimum separation distance. 4 points in a square are technically a poisson distribution.
Okay so update on my progress: I managed to find a Gaussian blur shader online and am currently trying to use that code to work. In the C# script pipeline I'm using, I have blit() being called in Update(). I feed in my RenderTexture which is then sent to a temporary render texture. The temporary render texture goes through the Gaussian Blue shader and then finally blit is called again to rewrite the original RenderTexture with the blurred version of itself. Unfortunately, this is not working and I don't know why. I've verified that the shader works by testing it out on the MainCamera and the game view is definitely blurred.
Further Update: Using the frame debugger I was able to determine that the render texture is being blurred (yay!) but is then being overridden. I don't exactly know why. What I do know is that the blur is the first effect to be processed, and then is overridden by a blit call that I don't exactly know where it's coming from as I'm fairly certain I've kept track of all my blits. Assistance or troubleshooting ideas would be greatly appreciated. EDIT: Hmm... It appears that the render texture is somehow being swapped from ARGB32 to Depth.
If you're doing the Blit() calls in Update(), you're likely modifying the previous frame's render texture, which then gets overridden when the current frame actually updates the depth texture. Basically all rendering starts after most of the C# code runs: https://docs.unity3d.com/Manual/ExecutionOrder.html Unless you're manually generating the depth texture using a cam.Render() call or similar, it'll absolutely be overridden.
Oh wow. I had no idea those functions existed! Much appreciated. I've switched over to OnRenderObject and tried out the other Render ones, but the same problem persists. Here's the problem I've identified: The RenderTexture that gets blurred is in an ARGB32 format. When the final blit is called to send it back to the original RenderTexture it's sent back in a Depth format, which for some reason erases the blurring. When I try setting the temporary RenderTextures to the Depth format they don't seem to hold anything. And then the final blit somehow sends in the depth map. I'm still messing around, but no luck so far. Step 1: Downsize Resolution by half Step 2: Horizontal Blur Step 3: Vertical Blur Step 4: Send back to original RenderTexture
OnRenderObject somewhat unintuitively runs after everything else has already rendered, so it's too late to be used by other objects in the scene. OnRenderImage runs after a camera has rendered, but only if the script is attached to a camera, and not if you're using camera.Render(), only if it renders normally (ie: it's enabled). I guess the real question is why you're copying it back to the original texture to begin with? Why not just blit to a new texture and use that in your material? Also, how are you creating your temporary render textures? I suspect you're not setting a format, or are using RenderTextureFormat.default. For this, you probably want to be using RenderTextureFormat.RHalf or some other single channel float format. I also almost never use the depth format, so I'm not sure if blit copies work with that format ... I would expect it to, but maybe it doesn't, especially if you're trying to copy from an RGBA32? I'd try making them all use the Depth format and see if that works. Really, without seeing your code I'm mostly just making wild guesses.
I seriously appreciate the help bgolus, it means a lot. Okay, so here's the code I'm working with: Code (CSharp): using UnityEngine; [ExecuteInEditMode] public class GaussianBlurFilter : MonoBehaviour { enum DownSampleMode { Off, Half, Quarter } [SerializeField] Shader _shader; [SerializeField] RenderTexture source; [SerializeField] DownSampleMode _downSampleMode = DownSampleMode.Quarter; [SerializeField, Range(0, 8)] int _iteration = 4; Material _material; void OnRenderObject() { if (_material == null) { _material = new Material(_shader); _material.hideFlags = HideFlags.HideAndDontSave; } RenderTexture rt1, rt2; if (_downSampleMode == DownSampleMode.Half) { rt1 = RenderTexture.GetTemporary(source.width / 2, source.height / 2); rt2 = RenderTexture.GetTemporary(source.width / 2, source.height / 2); Graphics.Blit(source, rt1); } else if (_downSampleMode == DownSampleMode.Quarter) { rt1 = RenderTexture.GetTemporary(source.width / 4, source.height / 4); rt2 = RenderTexture.GetTemporary(source.width / 4, source.height / 4); Graphics.Blit(source, rt1, _material, 0); } else { rt1 = RenderTexture.GetTemporary(source.width, source.height); rt2 = RenderTexture.GetTemporary(source.width, source.height); Graphics.Blit(source, rt1); } for (var i = 0; i < _iteration; i++) { Graphics.Blit(rt1, rt2, _material, 1); Graphics.Blit(rt2, rt1, _material, 2); } Graphics.Blit(rt1, source); RenderTexture.ReleaseTemporary(rt1); RenderTexture.ReleaseTemporary(rt2); } private void OnGUI() { GUI.DrawTexture(new Rect(0, 0, 256, 256), source); } } I haven't touched anything since your last response, so obviously a few things are going to not be optimal.
Currently it's just sitting in my texture folder. I drag and drop it into the inspector in the slot for "RenderTexture source" on line 12. However, I'm working on making it generated via a modified version of the script I posted above. It's updated via a camera's Target Texture slot.
Well, first thing, try making all of those GetTemporary use the source texture's format. RenderTexture.GetTemporary(source.width / 2, source.height / 2, 0, source.format); Best cast that fixes everything. Worst case nothing works at all anymore. I'm mostly curious how the original render texture is being filled.
Here's my updated code: Code (CSharp): using UnityEngine; public class SnowRenderTexturePipeline : MonoBehaviour { enum DownSampleMode { Off, Half, Quarter } [SerializeField] Material snowMat; [SerializeField] Shader blurShader; [SerializeField] DownSampleMode downSampleMode = DownSampleMode.Quarter; [SerializeField, Range(0, 8)] int iteration = 4; Material blurMat; RenderTexture depthTexture; Camera cam; private void Start() { depthTexture = new RenderTexture(512, 512, 24, RenderTextureFormat.Depth); cam = GetComponent<Camera>(); cam.targetTexture = depthTexture; } void OnRenderImage(RenderTexture source, RenderTexture destination) { if (blurMat == null) { blurMat = new Material(blurShader); blurMat.hideFlags = HideFlags.HideAndDontSave; } RenderTexture rt1, rt2; if (downSampleMode == DownSampleMode.Half) { rt1 = RenderTexture.GetTemporary(depthTexture.width / 2, depthTexture.height / 2, 24, RenderTextureFormat.Depth); rt2 = RenderTexture.GetTemporary(depthTexture.width / 2, depthTexture.height / 2, 24, RenderTextureFormat.Depth); Graphics.Blit(depthTexture, rt1); } else if (downSampleMode == DownSampleMode.Quarter) { rt1 = RenderTexture.GetTemporary(depthTexture.width / 4, depthTexture.height / 4, 24, RenderTextureFormat.Depth); rt2 = RenderTexture.GetTemporary(depthTexture.width / 4, depthTexture.height / 4, 24, RenderTextureFormat.Depth); Graphics.Blit(depthTexture, rt1, blurMat, 0); } else { rt1 = RenderTexture.GetTemporary(depthTexture.width, depthTexture.height, 24, RenderTextureFormat.Depth); rt2 = RenderTexture.GetTemporary(depthTexture.width, depthTexture.height, 24, RenderTextureFormat.Depth); Graphics.Blit(depthTexture, rt1); } for (var i = 0; i < iteration; i++) { Graphics.Blit(rt1, rt2, blurMat, 1); Graphics.Blit(rt2, rt1, blurMat, 2); } RenderTexture blurredDepthMap = new RenderTexture(depthTexture.width, depthTexture.height, 24, RenderTextureFormat.Depth); Graphics.Blit(rt1, blurredDepthMap); snowMat.SetTexture("_DispTex", blurredDepthMap); RenderTexture.ReleaseTemporary(rt1); RenderTexture.ReleaseTemporary(rt2); } private void OnGUI() { GUI.DrawTexture(new Rect(0, 0, 256, 256), depthTexture); } } Hmm.... So it appears nothing works anymore. The only thing that still works is the GUI.DrawTexture() call. The depth Texture shows up, but there's no displacement happening anymore. Here's the inspector for the camera in question in case you wanted to see that:
Okay, yeah, that confirms my expectations there. Use RenderTextureFormat.RHalf instead. Also set your snow camera's depth to -1 or lower, you want to make sure it's less than your main camera's depth. Other things, do not create a render texture every OnRenderImage. Create one during start and reuse it. make it the same resolution as the downscaled buffer, there's no point in bliting from a low resolution to a higher resolution.
Alright, I'll change those things and then report back my findings. UPDATE: Alright, I'm still getting the same results. Hmm... I did find this piece of info online. Not quite sure if it relates to what I'm doing though. https://support.unity3d.com/hc/en-u...aphics-Blit-does-not-copy-RenderTexture-depth
Well, I implemented the DepthCopy shader into my pipeline script in the most basic way possible and was able to replicate my existing functionality for the snow shader, so I think it's definitely related. Going to look into modifying the gaussian blur shader copy the depth in the same way. UPDATE: Getting somewhere... Got through the full script. No blur applied however. Hmm...
I'm getting closer. Okay, so I've managed to get the half and quarter size working, but I run into a problem. For some reason the resolution change and rescale is being wiped out by a Draw Dynamic event. Here's the info for the Draw Dynamic event in question: It's so bizarre. At this point I'm not using any ordinary blit calls anymore, and I can't identify what's causing it. UPDATE: Scratch that, it actually is working. Had to find a work around to doing an GUI.DrawTexture. Now to figure out how to apply the blur... UPDATE 2: No luck on applying the blur. Something seems to be wrong with the shader I think. Inputting custom values for the fragment shader doesn't do anything. Code (CSharp): Shader "Hidden/Gaussian Blur Filter" { Properties { _MainTex("-", 2D) = "white" {} _MyDepthTex("-", 2D) = "white" {} } CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; sampler2D_float _MyDepthTex; float4 _MyDepthTex_TexelSize; struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } // 9-tap Gaussian filter with linear sampling // http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/ half4 gaussian_filter(float2 uv, float2 stride) { half4 s = tex2Dlod(_MyDepthTex, float4(uv, 0, 0)) * 0.227027027; float2 d1 = stride * 1.3846153846; s += tex2Dlod(_MyDepthTex, float4(uv + d1, 0, 0)) * 0.3162162162; s += tex2Dlod(_MyDepthTex, float4(uv - d1, 0, 0)) * 0.3162162162; float2 d2 = stride * 3.2307692308; s += tex2Dlod(_MyDepthTex, float4(uv + d2, 0, 0)) * 0.0702702703; s += tex2Dlod(_MyDepthTex, float4(uv - d2, 0, 0)) * 0.0702702703; return s; } // Quarter downsampler half4 frag_quarter(v2f i, out float outDepth : SV_Depth) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_MyDepthTex, i.uv); outDepth = depth; float4 d = _MyDepthTex_TexelSize.xyxy * float4(1, 1, -1, -1); half4 s; s = tex2D(_MyDepthTex, i.uv + d.xy); s += tex2D(_MyDepthTex, i.uv + d.xw); s += tex2D(_MyDepthTex, i.uv + d.zy); s += tex2D(_MyDepthTex, i.uv + d.zw); return s * 0.25; } // Separable Gaussian filters half4 frag_blur_h(v2f i, out float outDepth : SV_Depth) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_MyDepthTex, i.uv); outDepth = depth; return gaussian_filter(i.uv, float2(_MyDepthTex_TexelSize.x, 0)); } half4 frag_blur_v(v2f i, out float outDepth : SV_Depth) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_MyDepthTex, i.uv); outDepth = depth; return gaussian_filter(i.uv, float2(0, _MyDepthTex_TexelSize.y)); } ENDCG Subshader { Pass { ZTest Always Cull Off ZWrite On CGPROGRAM #pragma vertex vert #pragma fragment frag_quarter ENDCG } Pass { ZTest Always Cull Off ZWrite On CGPROGRAM #pragma vertex vert #pragma fragment frag_blur_h #pragma target 3.0 ENDCG } Pass { ZTest Always Cull Off ZWrite On CGPROGRAM #pragma vertex vert #pragma fragment frag_blur_v #pragma target 3.0 ENDCG } } }
SOLVED IT! YES!! Okay, so in the end it was two key things that led to the solution. 1. blit() does not traditionally copy the depth value over to a new RenderTexture, so blit() needed to be modified to do that and the C# script had to be updated accordingly. 2. The fragment shader only returns values of color - no depth information at all! Instead, the fragment shader had to be modified to specifically blur the depth value, ignoring the ARGB values completely. Here's the final Gaussian blur shader: Code (CSharp): Shader "Hidden/Gaussian Blur Filter" { Properties { _MainTex("-", 2D) = "white" {} _MyDepthTex("-", 2D) = "white" {} } CGINCLUDE #include "UnityCG.cginc" sampler2D _MainTex; sampler2D_float _MyDepthTex; float4 _MyDepthTex_TexelSize; // 9-tap Gaussian filter with linear sampling // http://rastergrid.com/blog/2010/09/efficient-gaussian-blur-with-linear-sampling/ half gaussian_filter(float2 uv, float2 stride) { half s = tex2D(_MyDepthTex, float4(uv, 0, 0)).r * 0.227027027; float2 d1 = stride * 1.3846153846; s += tex2D(_MyDepthTex, uv + d1).r * 0.3162162162; s += tex2D(_MyDepthTex, uv - d1).r * 0.3162162162; float2 d2 = stride * 3.2307692308; s += tex2D(_MyDepthTex, uv + d2).r * 0.0702702703; s += tex2D(_MyDepthTex, uv - d2).r * 0.0702702703; return s; } // Quarter downsampler half4 frag_quarter(v2f_img i, out float outDepth : SV_Depth) : SV_Target { float depth = SAMPLE_DEPTH_TEXTURE(_MyDepthTex, i.uv); outDepth = depth; float4 d = _MyDepthTex_TexelSize.xyxy * float4(1, 1, -1, -1); half4 s; s = tex2D(_MyDepthTex, i.uv + d.xy); s += tex2D(_MyDepthTex, i.uv + d.xw); s += tex2D(_MyDepthTex, i.uv + d.zy); s += tex2D(_MyDepthTex, i.uv + d.zw); return s * 0.25; } // Separable Gaussian filters half4 frag_blur_h(v2f_img i, out float outDepth : SV_Depth) : SV_Target { outDepth = gaussian_filter(i.uv, float2(_MyDepthTex_TexelSize.x, 0)); return 0; } half4 frag_blur_v(v2f_img i, out float outDepth : SV_Depth) : SV_Target { outDepth = gaussian_filter(i.uv, float2(0, _MyDepthTex_TexelSize.y)); return 0; } ENDCG Subshader { Pass { ZTest Always Cull Off ZWrite On CGPROGRAM #pragma vertex vert_img #pragma fragment frag_quarter ENDCG } Pass { ZTest Always Cull Off ZWrite On CGPROGRAM #pragma vertex vert_img #pragma fragment frag_blur_h #pragma target 3.0 ENDCG } Pass { ZTest Always Cull Off ZWrite On CGPROGRAM #pragma vertex vert_img #pragma fragment frag_blur_v #pragma target 3.0 ENDCG } } }
Thanks for being awesome enough to share the journey! Most people don't post when they've resolved it
Yeah no problem! The results are just what I wanted too! No blur applied: Blur applied: Also, special shout out to bgolus for being generally awesome and helping me troubleshoot!
Yep @bgolus is a gem on these forums. He's basically erased all the guesswork and confusion that was here before.