{"id":471,"date":"2021-07-25T00:02:29","date_gmt":"2021-07-25T00:02:29","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=471"},"modified":"2021-07-25T00:02:30","modified_gmt":"2021-07-25T00:02:30","slug":"pixelate-filter-in-urp-using-compute-shaders","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/07\/25\/pixelate-filter-in-urp-using-compute-shaders\/","title":{"rendered":"Pixelate Filter in URP using Compute Shaders"},"content":{"rendered":"\n<p>This article will use the pixelate image filter from the previous post but in URP, the Universal Render Pipeline. We&#8217;ll set up a <code>ScriptableRendererFeature<\/code> and <code>ScriptableRenderPass<\/code> to run the compute shader at the end of the rendering pipeline. I recommend you read 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\/\">previous pixelate filter post<\/a>&nbsp;before this one to understand the full context.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Scriptable Renderer Features<\/h3>\n\n\n\n<p>In case you forgot, Scriptable Renderer Features are the mechanism through which we can extend the Universal Rendering Pipeline. They pair with Scriptable Render Passes. A Scriptable Render Pass is a small, potentially reusable sequence of rendering instructions, and a Scriptable Renderer Feature is a collection of one or more passes.<\/p>\n\n\n\n<p>Let&#8217;s breakdown what we need our feature to do:<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Access the camera texture after rendering.<\/li><li>Blit (copy) the camera texture to another render texture.<\/li><li>Send that render texture to our pixelate compute shader.<\/li><li>Run the compute shader.<\/li><li>Copy the render texture back to camera texture, where it will ultimately find its way back to the screen.<\/li><\/ol>\n\n\n\n<p>There&#8217;s no reason to break this up into multiple passes, so it&#8217;ll be a feature that runs a single pass. In theory, we could generalize the render pass to run any image filter compute shader, but frankly, the added complexity isn&#8217;t worth it.<\/p>\n\n\n\n<p>Let&#8217;s start with the meat, that is, the execute function of the render pass.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pixelate Render Pass<\/h3>\n\n\n\n<p>As mentioned, we&#8217;ll start with the <code>Execute<\/code> method and work our way backwards. The execute method does what I outlined in the previous breakdown. The rest of the code we&#8217;ll write is either set up or boilerplate. To send commands to the GPU, we need a <code>CommandBuffer<\/code>. To use a command buffer, we declare a sequence of commands to execute and, well, execute them. So, first, we ask the <code>CommandBufferPool<\/code> for the next available <code>CommandBuffer<\/code>. Then, we <code>Blit<\/code> the camera texture to a temporary render texture, set all the compute shader parameters, dispatch the compute shader, and finally <code>Blit<\/code> the temporary render texture back to the camera target texture. Finally, at the end, we&#8217;ll execute the command buffer and release all our <code>CommandBuffer<\/code> back into the pool.<\/p>\n\n\n\n<p>There&#8217;s one last caveat. This render pass will attempt to execute on every camera, including the scene view camera. We&#8217;ll prevent it from running in the scene view by exiting early.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)\n{\n    if (renderingData.cameraData.isSceneViewCamera)\n        return;\n\n    CommandBuffer cmd = CommandBufferPool.Get();\n    var mainKernel = _filterComputeShader.FindKernel(_kernelName);\n    _filterComputeShader.GetKernelThreadGroupSizes(mainKernel, out uint xGroupSize, out uint yGroupSize, out _);\n    cmd.Blit(renderingData.cameraData.targetTexture, _renderTargetIdentifier);\n    cmd.SetComputeTextureParam(_filterComputeShader, mainKernel, _renderTargetId, _renderTargetIdentifier);\n    cmd.SetComputeIntParam(_filterComputeShader, \"_BlockSize\", _blockSize);\n    cmd.SetComputeIntParam(_filterComputeShader, \"_ResultWidth\", renderTextureWidth);\n    cmd.SetComputeIntParam(_filterComputeShader, \"_ResultHeight\", renderTextureHeight);\n    cmd.DispatchCompute(_filterComputeShader, mainKernel,\n        Mathf.CeilToInt(_renderTextureWidth \/ (float) _blockSize \/ xGroupSize),\n        Mathf.CeilToInt(_renderTextureHeight \/ (float) _blockSize \/ yGroupSize),\n        1);\n    cmd.Blit(_renderTargetIdentifier, renderingData.cameraData.renderer.cameraColorTarget);\n\n    context.ExecuteCommandBuffer(cmd);\n    cmd.Clear();\n    CommandBufferPool.Release(cmd);\n}<\/code><\/pre>\n\n\n\n<p>So that&#8217;s our render pass. Now we&#8217;ll build up all the boilerplate around it. First, let&#8217;s get our render texture. Scriptable Render Passes have <code>OnCameraSetup<\/code> and <code>OnCameraCleanup<\/code> callbacks to configure and release any resources you need. Each of these callbacks receives a <code>CommandBuffer<\/code> of their own, which the rendering pipeline executes automatically at the appropriate time. We&#8217;ll use it to ask for a temporary render texture and then later release it. To request a temporary render texture, we need to provide a descriptor. The descriptor contains information like the width, height, pixel format, etc. Since we want a texture that&#8217;s identical to the camera&#8217;s target texture, we reuse that descriptor with <code>enableRandomWrite<\/code> enabled because otherwise, the compute shader can&#8217;t write to it. Additionally, we&#8217;ll cache the width and height of the render texture to pass it to the compute shader later.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)\n{\n    var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;\n    cameraTargetDescriptor.enableRandomWrite = true;\n    cmd.GetTemporaryRT(_renderTargetId, cameraTargetDescriptor);\n    _renderTargetIdentifier = new RenderTargetIdentifier(_renderTargetId);\n\n    _renderTextureWidth = cameraTargetDescriptor.width;\n    _renderTextureHeight = cameraTargetDescriptor.height;\n}\n\npublic override void OnCameraCleanup(CommandBuffer cmd)\n{\n    cmd.ReleaseTemporaryRT(_renderTargetId);\n}<\/code><\/pre>\n\n\n\n<p>To compile, we&#8217;re still missing a handful of variables. These leftover variables are the kind that would be nice to adjust through the Inspector. These include the compute shader, the pixel block size, the kernel name, and the name of the render texture. So, let&#8217;s write a constructor that takes these arguments, and we&#8217;ll pass them from the Scriptable Renderer Feature. That&#8217;s because the Scriptable Renderer Feature can have serialized fields that are assignable via the Inspector. Here&#8217;s the constructor.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public PixelateRenderPass(ComputeShader filterComputeShader, string kernelName, int blockSize, int renderTargetId)\n{\n    _filterComputeShader = filterComputeShader;\n    _kernelName = kernelName;\n    _blockSize = blockSize;\n    _renderTargetId = renderTargetId;\n}<\/code><\/pre>\n\n\n\n<p>Next, let&#8217;s implement the Scriptable Renderer Feature.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Pixelate Scriptable Renderer Feature<\/h3>\n\n\n\n<p>Scriptable Renderer Features have two important methods: <code>Create<\/code> and <code>AddRenderPasses<\/code>. <code>Create<\/code> is the constructor equivalent, or <code>Start<\/code> if you&#8217;re used to MonoBehaviours. <code>AddRenderPasses<\/code> is where we enqueue the render passes. Remember when I said Scriptable Renderer Features contained one or more passes? Well, <code>AddRenderPasses<\/code> is where you add the collection of passes to the render pipeline. Let&#8217;s start with the serialized fields.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class ComputeShaderPixelateImageFilter : ScriptableRendererFeature\n{\n\tPixelateRenderPass _scriptablePass;\n\tbool _initialized;\n\n\tpublic ComputeShader FilterComputeShader;\n\tpublic string KernelName = \"Pixelate\";\n\t&#91;Range(2, 40)] public int BlockSize = 3;\n}<\/code><\/pre>\n\n\n\n<p>These fields are modifiable in the Inspector, like any standard MonoBehaviour. They&#8217;re also the same variables we said we&#8217;d pass into the render pass constructor.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override void Create()\n{\n    if (FilterComputeShader == null)\n    {\n        _initialized = false;\n        return;\n    }\n    \n    int renderTargetId = Shader.PropertyToID(\"_ImageFilterResult\");\n    _scriptablePass = new PixelateRenderPass(FilterComputeShader, KernelName, BlockSize, renderTargetId);\n    _scriptablePass.renderPassEvent = RenderPassEvent.AfterRendering;\n    _initialized = true;\n}<\/code><\/pre>\n\n\n\n<p>Here&#8217;s the create function. Aside from constructing the render pass, there are two items of note here. First, what&#8217;s the <code>renderTargetId<\/code>? It&#8217;s the name of the render texture on the GPU, converted into an Id. By the way, we&#8217;ll use the same name for the result texture in the Pixelate compute shader. Second, what&#8217;s <code>renderPassEvent<\/code>? It&#8217;s is the point within the render pipeline where our render pass will run. Since we want to run the image filter at the end of the pipeline, we set it to <code>AfterRendering<\/code>.<\/p>\n\n\n\n<p>Finally, let&#8217;s move on to the <code>AddRenderPasses<\/code> method.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)\n{\n    if (_initialized)\n    {\n        renderer.EnqueuePass(_scriptablePass);\n    }\n}<\/code><\/pre>\n\n\n\n<p>It&#8217;s remarkably straightforward; we enqueue the pass. So that&#8217;s all the code. I&#8217;ll drop the entire file here for posterity, but I recommend you check it out on Github if you&#8217;re interested in exploring the project. The Github link is at the bottom of this article.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using UnityEngine;\nusing UnityEngine.Rendering;\nusing UnityEngine.Rendering.Universal;\n\npublic class ComputeShaderPixelateImageFilter : ScriptableRendererFeature\n{\n    #region Renderer Pass\n    class PixelateRenderPass : ScriptableRenderPass\n    {\n        ComputeShader _filterComputeShader;\n        string _kernelName;\n        int _renderTargetId;\n\n        RenderTargetIdentifier _renderTargetIdentifier;\n        int _renderTextureWidth;\n        int _renderTextureHeight;\n\n        int _blockSize;\n\n        public PixelateRenderPass(ComputeShader filterComputeShader, string kernelName, int blockSize, int renderTargetId)\n        {\n            _filterComputeShader = filterComputeShader;\n            _kernelName = kernelName;\n            _blockSize = blockSize;\n            _renderTargetId = renderTargetId;\n        }\n\n        public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)\n        {\n            var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;\n            cameraTargetDescriptor.enableRandomWrite = true;\n            cmd.GetTemporaryRT(_renderTargetId, cameraTargetDescriptor);\n            _renderTargetIdentifier = new RenderTargetIdentifier(_renderTargetId);\n\n            _renderTextureWidth = cameraTargetDescriptor.width;\n            _renderTextureHeight = cameraTargetDescriptor.height;\n        }\n\n        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)\n        {\n            if (renderingData.cameraData.isSceneViewCamera)\n                return;\n\n            CommandBuffer cmd = CommandBufferPool.Get();\n            var mainKernel = _filterComputeShader.FindKernel(_kernelName);\n            _filterComputeShader.GetKernelThreadGroupSizes(mainKernel, out uint xGroupSize, out uint yGroupSize, out _);\n            cmd.Blit(renderingData.cameraData.targetTexture, _renderTargetIdentifier);\n            cmd.SetComputeTextureParam(_filterComputeShader, mainKernel, _renderTargetId, _renderTargetIdentifier);\n            cmd.SetComputeIntParam(_filterComputeShader, \"_BlockSize\", _blockSize);\n            cmd.SetComputeIntParam(_filterComputeShader, \"_ResultWidth\", _renderTextureWidth);\n            cmd.SetComputeIntParam(_filterComputeShader, \"_ResultHeight\", _renderTextureHeight);\n            cmd.DispatchCompute(_filterComputeShader, mainKernel,\n                Mathf.CeilToInt(_renderTextureWidth \/ (float) _blockSize \/ xGroupSize),\n                Mathf.CeilToInt(_renderTextureHeight \/ (float) _blockSize \/ yGroupSize),\n                1);\n            cmd.Blit(_renderTargetIdentifier, renderingData.cameraData.renderer.cameraColorTarget);\n\n            context.ExecuteCommandBuffer(cmd);\n            cmd.Clear();\n            CommandBufferPool.Release(cmd);\n        }\n\n        public override void OnCameraCleanup(CommandBuffer cmd)\n        {\n            cmd.ReleaseTemporaryRT(_renderTargetId);\n        }\n    }\n\n    #endregion\n\n    #region Renderer Feature\n\n    PixelateRenderPass _scriptablePass;\n    bool _initialized;\n    \n    public ComputeShader FilterComputeShader;\n    public string KernelName = \"Pixelate\";\n    &#91;Range(2, 40)] public int BlockSize = 3;\n\n    public override void Create()\n    {\n        if (FilterComputeShader == null)\n        {\n            _initialized = false;\n            return;\n        }\n        \n        int renderTargetId = Shader.PropertyToID(\"_ImageFilterResult\");\n        _scriptablePass = new PixelateRenderPass(FilterComputeShader, KernelName, BlockSize, renderTargetId);\n        _scriptablePass.renderPassEvent = RenderPassEvent.AfterRendering;\n        _initialized = true;\n    }\n\n    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)\n    {\n        if (_initialized)\n        {\n            renderer.EnqueuePass(_scriptablePass);\n        }\n    }\n\n    #endregion\n}<\/code><\/pre>\n\n\n\n<p>The pixelate compute shader is nearly unchanged from the previous post. The only difference is I changed the name of the result render texture to <code>_ImageFilterResult<\/code> to be more descriptive. At this point, you can add this Renderer Feature to your URP renderer, set the inspector values, and you&#8217;re good to go.<\/p>\n\n\n\n<p>So now we have a template for fullscreen Compute Shader effects in URP. That means we can write Compute Shader image effects and reuse them in more projects. Compute Shaders, unlike regular Shaders, are rendering pipeline agnostic, so this is a win.<\/p>\n\n\n\n<p><strong>Explore the project&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/PixelatePostProcessingURP\"><strong>here<\/strong><\/a><strong>&nbsp;on GitHub. If my work is helpful to you,&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/join-my-mailing-list\/\"><strong>join 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>This article will use the pixelate image filter from the previous post but in URP, the Universal Render Pipeline. We&#8217;ll set up a ScriptableRendererFeature and ScriptableRenderPass to run the compute shader at the end of the rendering pipeline. I recommend you read the&nbsp;previous pixelate filter post&nbsp;before this one to understand the full context. Scriptable Renderer [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":474,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[25,17,1],"tags":[26,8,41,40,13],"class_list":["post-471","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-image-filter","tag-post-processing","tag-universal-render-pipeline"],"_links":{"self":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/471","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=471"}],"version-history":[{"count":3,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/471\/revisions"}],"predecessor-version":[{"id":475,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/471\/revisions\/475"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media\/474"}],"wp:attachment":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media?parent=471"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=471"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=471"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}