{"id":477,"date":"2021-08-01T04:12:43","date_gmt":"2021-08-01T04:12:43","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=477"},"modified":"2021-08-14T17:14:41","modified_gmt":"2021-08-14T17:14:41","slug":"pixelate-filter-in-hdrp-using-compute-shaders","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/08\/01\/pixelate-filter-in-hdrp-using-compute-shaders\/","title":{"rendered":"Pixelate Filter in HDRP using Compute Shaders"},"content":{"rendered":"\n<p>In this article, we&#8217;ll complete the trifecta and port our Pixelate image filter to HDRP. If you haven&#8217;t yet, I recommend reading the&nbsp;<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/2021\/07\/17\/pixelate-filter-post-processing-in-a-compute-shader\/\">first post<\/a>&nbsp;first to understand the full context. This post will focus on the changes required to make the compute shader filter work in HDRP, but we won&#8217;t focus on the compute shader itself.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">CustomPostProcessVolumeComponent<\/h3>\n\n\n\n<p>For this HDRP version, we&#8217;ll use a Custom Post-Process Volume Component. This is HDRP&#8217;s system for extending the renderer with more post-processing. HDRP also supports custom rendering passes, but that&#8217;s intended to add new ways to render objects rather than fullscreen effects.<\/p>\n\n\n\n<p>The <code>CustomPostProcessVolumeComponent<\/code> has a <code>Render<\/code> method that we can override. The <code>Render<\/code> method receives a <code>CommandBuffer<\/code>, an <code>HDCamera<\/code>, and two render texture handles for the source and destination. So, we&#8217;ll set up the Pixelate Compute Shader variables and dispatch the shader. By the way, if you&#8217;ve been following along with the series, it&#8217;s important to mention that I&#8217;ve modified the Pixelate Compute Shader in this version. For one, I&#8217;ve added another <code>RWTexture<\/code> to hold the source texture. As a result, we can read from the source and write to the destination. This way saves us from having to blit from the source to the destination before dispatching the filter.<\/p>\n\n\n\n<p>Additionally, there are some HDRP macros to declare RWTextures and index into them as well. Without these macros, the shader won&#8217;t run in DirectX. So here&#8217;s the modified <code>Pixelate.compute<\/code> file; notice the use of the <code>RW_TEXTURE2D_X<\/code> and the <code>COORD_TEXTURE2D_X<\/code> macros.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#include \"Packages\/com.unity.render-pipelines.core\/ShaderLibrary\/Common.hlsl\"\n#include \"Packages\/com.unity.render-pipelines.high-definition\/Runtime\/ShaderLibrary\/ShaderVariables.hlsl\"\n\n#pragma kernel Pixelate\n\n<strong>RW_TEXTURE2D_X<\/strong>(float4, _ImageFilterSource);\n<strong>RW_TEXTURE2D_X<\/strong>(float4, _ImageFilterResult);\n\nint _BlockSize;\nuint _ResultWidth;\nuint _ResultHeight;\n\n&#91;numthreads(8,8,1)]\nvoid Pixelate (uint3 id : SV_DispatchThreadID)\n{\n    if (id.x &gt;= _ResultWidth || id.y &gt;= _ResultHeight)\n        return;\n\n    const uint2 startPos = id.xy * _BlockSize;\n    \n    if (startPos.x &gt;= _ResultWidth || startPos.y &gt;= _ResultHeight)\n        return;\n    \n    const int blockWidth = min(_BlockSize, _ResultWidth - startPos.x);\n    const int blockHeight = min(_BlockSize, _ResultHeight - startPos.y);\n    const int numPixels = blockHeight * blockWidth;\n    \n    float4 colour = float4(0, 0, 0, 0);\n    for (int x = 0; x &lt; blockWidth; ++x)\n    {\n        for (int y = 0; y &lt; blockHeight; ++y)\n        {\n            const uint2 pixelPos = uint2(startPos.x + x, startPos.y + y);\n            colour += _ImageFilterSource&#91;<strong>COORD_TEXTURE2D_X<\/strong>(pixelPos)];\n        }\n    }\n    colour \/= numPixels;\n\n    for (int i = 0; i &lt; blockWidth; ++i)\n    {\n        for (int j = 0; j &lt; blockHeight; ++j)\n        {\n            const uint2 pixelPos = uint2(startPos.x + i, startPos.y + j);\n            _ImageFilterResult&#91;<strong>COORD_TEXTURE2D_X<\/strong>(pixelPos)] = colour;\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>With that context, here&#8217;s the <code>Render<\/code> method from the <code>CustomPostProcessVolumeComponent<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)\n{\n    var mainKernel = FilterComputeShader.FindKernel(\"Pixelate\");\n    FilterComputeShader.GetKernelThreadGroupSizes(mainKernel, out uint xGroupSize, out uint yGroupSize, out _);\n    cmd.SetComputeTextureParam(FilterComputeShader, mainKernel, \"_ImageFilterSource\", source.nameID);\n    cmd.SetComputeTextureParam(FilterComputeShader, mainKernel, \"_ImageFilterResult\", destination.nameID);\n    cmd.SetComputeIntParam(FilterComputeShader, \"_BlockSize\", BlockSize.value);\n    cmd.SetComputeIntParam(FilterComputeShader, \"_ResultWidth\", destination.rt.width);\n    cmd.SetComputeIntParam(FilterComputeShader, \"_ResultHeight\", destination.rt.height);\n    cmd.DispatchCompute(FilterComputeShader, mainKernel,\n        Mathf.CeilToInt(destination.rt.width \/ (float) BlockSize.value \/ xGroupSize),\n        Mathf.CeilToInt(destination.rt.height \/ (float) BlockSize.value \/ yGroupSize),\n        1);\n}<\/code><\/pre>\n\n\n\n<p>As you can see, we sequence the <code>CommandBuffer<\/code> with a series of commands to set all the Compute Shader variables and dispatch the shader. There&#8217;s no other boilerplate necessary this time. However, there are a few adjustable parameters that we should expose in the inspector, so we&#8217;ll cover that next.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setting Parameters<\/h3>\n\n\n\n<p>A <code>CustomPostProcessVolumeComponent<\/code> can have public parameters that appear in the inspector. However, we must wrap them in a <code>VolumeParameter<\/code> class; otherwise, they won&#8217;t show up. The reason we wrap the parameters is so that the system can handle enabling and disabling effect overrides. Additionally, it forces us to set a reasonable default value. We&#8217;ll add an <code>int<\/code> to control the pixel block size, a <code>bool<\/code> to toggle the effect in the scene view, and a <code>ComputeShader<\/code> field. There are built-in classes for <code>IntParameter<\/code> and <code>BoolParameter<\/code>, so that&#8217;s easy. By the way, <code>CustomPostProcessVolumeComponent<\/code> has an overrideable property to control whether it&#8217;s visible in the scene view. So let&#8217;s connect that as well.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public ClampedIntParameter BlockSize = new ClampedIntParameter(5, 2, 20);\npublic BoolParameter ShowInSceneView = new BoolParameter(false);\n\npublic override bool visibleInSceneView =&gt; ShowInSceneView.value;<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Custom Parameters<\/h3>\n\n\n\n<p>To supply a <code>ComputeShader<\/code>, we need a new class that inherits from <code>VolumeParameter<\/code>. Let&#8217;s create the class and write a constructor.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using UnityEngine;\nusing UnityEngine.Rendering;\n\n&#91;System.Serializable]\npublic class ComputeShaderParameter : VolumeParameter&lt;ComputeShader&gt;\n{\n    public ComputeShaderParameter(ComputeShader value, bool overrideState = false)\n        : base(value, overrideState)\n    {\n    }\n}<\/code><\/pre>\n\n\n\n<p>Now jump back to the post-process volume and add another parameter. We also want to disable the effect if there&#8217;s no supplied compute shader. To do this, we can add an <code>IsActive<\/code> method which comes from inheriting <code>IPostProcessComponent<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public ComputeShaderParameter FilterComputeShaderParameter = new ComputeShaderParameter(null);\n\npublic bool IsActive() =&gt; FilterComputeShaderParameter.value != null;<\/code><\/pre>\n\n\n\n<p>Finally, we must specify the injection point, that is, the point in the render pipeline when this effect will run. To do this, we override the property called <code>injectionPoint<\/code>. In our case, we want to run last, so we&#8217;ll use <code>AfterPostProcess<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override CustomPostProcessInjectionPoint injectionPoint =&gt; CustomPostProcessInjectionPoint.AfterPostProcess;<\/code><\/pre>\n\n\n\n<p>This completes the image filter code. However, even if you create a <code>Volume<\/code> in your scene and add the effect, you won&#8217;t see it. That&#8217;s because we have to add it to the Project Settings as well. Open Edit &gt; Project Settings, select the tab HDRP Default Settings, then scroll down to Custom Post Process Orders. Here we can add our effect to the desired injection point, After Post Process. Now, you can add the effect to a volume in your scene and bask in the high def fuzzy pixel glory. Try automating the <code>BlockSize<\/code> to make amazing pixel wipe effects.<\/p>\n\n\n\n<p>Here&#8217;s the entire image filter code.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using UnityEngine;\nusing UnityEngine.Rendering;\nusing UnityEngine.Rendering.HighDefinition;\nusing System;\n\n&#91;Serializable, VolumeComponentMenu(\"Post-processing\/Custom\/PixelateImageFilter\")]\npublic sealed class PixelateImageFilter : CustomPostProcessVolumeComponent, IPostProcessComponent\n{\n    public ClampedIntParameter BlockSize = new ClampedIntParameter(5, 2, 20);\n    public BoolParameter ShowInSceneView = new BoolParameter(false);\n    public ComputeShaderParameter FilterComputeShaderParameter = new ComputeShaderParameter(null);\n    \n    public bool IsActive() =&gt; FilterComputeShaderParameter.value != null;\n\n    public override bool visibleInSceneView =&gt; ShowInSceneView.value;\n\n    public override CustomPostProcessInjectionPoint injectionPoint =&gt; CustomPostProcessInjectionPoint.AfterPostProcess;\n\n    public override void Render(CommandBuffer cmd, HDCamera camera, RTHandle source, RTHandle destination)\n    {\n        var filterComputeShader = FilterComputeShaderParameter.value;\n        var mainKernel = filterComputeShader.FindKernel(\"Pixelate\");\n        filterComputeShader.GetKernelThreadGroupSizes(mainKernel, out uint xGroupSize, out uint yGroupSize, out _);\n        cmd.SetComputeTextureParam(filterComputeShader, mainKernel, \"_ImageFilterSource\", source.nameID);\n        cmd.SetComputeTextureParam(filterComputeShader, mainKernel, \"_ImageFilterResult\", destination.nameID);\n        cmd.SetComputeIntParam(filterComputeShader, \"_BlockSize\", BlockSize.value);\n        cmd.SetComputeIntParam(filterComputeShader, \"_ResultWidth\", destination.rt.width);\n        cmd.SetComputeIntParam(filterComputeShader, \"_ResultHeight\", destination.rt.height);\n        cmd.DispatchCompute(filterComputeShader, mainKernel,\n            Mathf.CeilToInt(destination.rt.width \/ (float) BlockSize.value \/ xGroupSize),\n            Mathf.CeilToInt(destination.rt.height \/ (float) BlockSize.value \/ yGroupSize),\n            1);\n    }\n}<\/code><\/pre>\n\n\n\n<p>So we&#8217;ve finally completed the trifecta. We can now write Compute Shader post-processing effects across all through rendering pipelines. Unfortunately, we had to use HDRP macros in this version, so the shader is no longer entirely cross-compatible. If you find yourself in a position where you need that compatibility, I suggest implementing your own macros to branch according to the renderer you&#8217;re using.<\/p>\n\n\n\n<p><strong>Check out the project&nbsp;<\/strong><a href=\"https:\/\/github.com\/bzgeb\/PixelatePostProcessingHDRP\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>here<\/strong><\/a><strong>&nbsp;on GitHub. If you like my work, consider&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/join-my-mailing-list\/\"><strong>joining my mailing list<\/strong><\/a><strong>, and I&#8217;ll email you whenever a new post is released.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this article, we&#8217;ll complete the trifecta and port our Pixelate image filter to HDRP. If you haven&#8217;t yet, I recommend reading the&nbsp;first post&nbsp;first to understand the full context. This post will focus on the changes required to make the compute shader filter work in HDRP, but we won&#8217;t focus on the compute shader itself. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":485,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[25,17,1],"tags":[26,8,47,48,5],"class_list":["post-477","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-compute-shaders","category-graphics","category-unity-programming","tag-compute-shaders","tag-game-development","tag-hdrp","tag-high-definition-render-pipeline","tag-unity"],"_links":{"self":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/477","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/comments?post=477"}],"version-history":[{"count":9,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/477\/revisions"}],"predecessor-version":[{"id":500,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/477\/revisions\/500"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media\/485"}],"wp:attachment":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media?parent=477"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=477"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=477"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}