Unofficial Hotscreen Community
Dual Pass Gaussian Blur - Printable Version

+- Unofficial Hotscreen Community (https://hotscreen.dominated.dev)
+-- Forum: HotScreen (https://hotscreen.dominated.dev/forumdisplay.php?fid=6)
+--- Forum: Mods (https://hotscreen.dominated.dev/forumdisplay.php?fid=12)
+--- Thread: Dual Pass Gaussian Blur (/showthread.php?tid=80)



Dual Pass Gaussian Blur - Radack - 05-14-2026

This is a dual pass implementation of a gaussian blur filter. If you want to know what that means you can read the sections below that explain it in detail. Basically it is a way more efficient implementation with no quality loss. If you just want to goon, read the first section and skip the rest. Please let me know if this works on your PC. As per my current understanding, this implementation shouldn't work but it does (you can read about that in the last section if you care). I probably won't be able to do shit about it if it doesn't work, but im curious nonetheless. 
Shoutout to lamba5da and siamirold for creating their single pass variants, which inspired me to do this!
You are free to use this code as you see fit, but please do credit me if you decide to publish any mods based on this work! In theory, you should be able to reuse most of this code for any dual pass shader, you just need to replace the shader code. 
Also, fuck LLMs. I swear, learning how to do this myself was faster than the days i spent asking Gemini to do it (<- this might be a skill issue).


How do i use this for gooning purposes?

Just download the attached .zip file and unpack it. Put the .box.tscn file in your CUSTOM_DATA folder. Then add a new filter to your Hotscreen and select Mods>Box Scene. Click "Edit filter" and click on "Select custom scene". Select the file you just downloaded and enjoy! If you want to change the strength of the blur you have to open the .box.tscn file with any text editor, change the "sigma" parameter in line 9 to your desired strength (higher = stronger blur) and then reload the scene in Hotscreen ("Select custom scene"). You can set sigma higher than 50, however you need to adjust line 67 (haha 67 lmao) in the code (rect = rect.grow(100)). Simply set the parameter of grow() to 2*sigma and you are good. If you set sigma lower than 50 you do not need to adjust this paramter. If you want to know how and why it works this way read the sections below.
There are a few "quirks" you should be aware of:
- This filter does not work with the "Inverted boxes" feature of Hotscreen. For whatever reason it breaks the second blur pass, leaving you with an image that is only blurred in one direction. If you want something similiar set the size of the filter to maximum and use an "Always display screen" filter below this one.
- If your PC enters standby mode while Hotscreen is running or if you pause Hotscreen with this filter active, the filter will not work anymore. It will just output a solid black color where the blur is supposed to be. In this case you have to restart Hotscreen to make it work again.
- Please be aware that while this is more performant than the single pass variant, it can still be performance intensive if you blur large areas of the screen with a high sigma value.
- This filter "stacks" with other filter that are shown below it. I believe this is due to how i sample the screen texture in my code, but I am honestly not sure and too lazy to test it. I personally think this is a pretty neat feature. If it bothers you for some reason, you'll have to adjust the code to use the "normal Hotscreen way" of sampling the screen texture (or ask an LLM to do it). You can find the "normal way" in the prompt for a custom shader effect inside Hotscreen
- I am sure there are other things that could go wrong. Let me know if you find any! (Can't promise I'll be able to fix it though!)

The rest of this post discusses some technical details that are not relevant if you just want to use the filter.


What even is the problem?

If you have used one of the gaussian blur shaders available on this website, you probably noticed they have a pretty heavy performance impact. This is due to how many pixels they have to sample. A single pass gaussian blur shader with radius 10 has to sample 10*10=100 pixels in a square grid. For radius 50 that's 50*50=2500 pixels. Generally, for a radius of n pixels you need to sample n*n pixels. And you have to do that for every single pixel you want to blur! That is very performance expensive, especially if you need to perform some arithmetic operations on all of those pixels (which we do, since we apply a shader to them). 


The solution

A gaussian blur shader (or to be more precise the gaussian blur convolution kernel) is separable. What does that mean? You can split it into two distinct passes that each blur in different directions: the first pass blurs the image vertically, the second pass blurs it horizontally or vice versa. This singnificantly reduces the amount of work you have to do, as you no longer sample pixels in a square grid but in two lines (one horizontal, one vertical). For a blur with radius 10 you now only need to sample 10*2=20 pixels and for radius 50 you only need 50*2=100 pixels. As you can see, we have significantly lowered the amount of pixels we need to sample and subsequently perform arithmetic operations on. Now we only need to sample 2*n pixels for a blur with radius n. 


How does this work?

This part is based on my understanding of things. Not everything is necessarily going to be 100% correct. If you know more about any of this than I do feel free to educate me! 
Okay, so basics first: We are using a canvas_item shader in Godot to apply our blur. This shader is simply applied to all pixels in our image. If we apply a shader to a certain Node in Godot, only the parts of the image covered by that Node are affected by the shader. A Node is any object in Godot, for example a ColorRect or a Polygon2D (if you don't know what those are you'll have to look it up)
Implementing a multi pass blur in a single shader is (as far as I am aware) not possible, because we iterate over each pixels only once This is why we need to construct our own Godot scene. Our parent node is a Polygon2D as it is required by Hotscreen. Hotscreen manipulates this Polygon2D to cover anything naughty on the screen. A Polygon2D is just a collection of points that form a shape (in the case of Hotscreen a rectangle). We can apply our first blur pass to this Polygon2D as a shader. 
Pretty easy so far, right? Now comes the tricky part. We could just create a copy of our Polygon2D, apply the second pass to it and be done with it. That is what I tried at first, however it did not work out very well. The second pass of the blur assumes that all the pixels it samples have already been blurred previously. This i no problem for most of our pixels. However, if we are near the edge of our Polygon2D and our radius is sufficiently high, we will be trying to sample pixels that are outside our Polygon. These pixels obviously have not been blurred previously, which will lead to streaky artifacts in our blurred output. You can see this for yourself if you set the parameter of grow() to a number smaller than sigma. Depending on the image you are viewing these artifacts may be more or less visible. 
So in our first pass we have to somehow blur an area that is larger than the area we are actually trying to censor. This is handled by the custom script in the scene file. This script computes the bounding box of our Polygon2D and then expands it by 100 pixels. The bounding box is essentially just a rectangle that fits all points of our Polygon2D inside of it. We then place a ColorRect inside our bounding box and apply the other blur pass to it as a shader. Now our ColorRect blurs a larger area than our Polygon2D. Since the ColorRect is a child of our Polygon2D Godot draws it first. So our ColorRect is drawn first, applies the first pass, then our Polygon2D is drawn on top of it and apllies the second pass.
Now we just have one more problem to solve: our ColorRect is still visible in its entirety. The solution is pretty simple: we just have to enable clipping on our Polygon2D. This basically uses the Polygon2D as a mask and only draws our ColorRect inside the Polygon2D. Everything else is cut off. Now we have a clean dual pass implementation for our gaussian blur! Yippie! We are done! 


The problem with the solution

...excpet I lied to you. See, in Godot parents are drawn first and then their children are drawn afterwards. So in theory, our Polygon2D is drawn first and then our ColorRect is drawn on top of it. However, that would mean, that our ColorRect is sampling unblurred pixels which should lead to artifacts. You can see this if you set clip_children=0 in the code and pay attention to the edges of our previously blurred area. And this is where my understanding of the Godot rendering pipeline ends. In Godot there are two ways of clipping children: "Clip only" which means the parent does not get drawn and "Clip + Draw" which does draw the parent. This filter only works, if you set the clipping to "Clip only" (which corresponds to clip_childern=1 in the code). That means the parent (our Polygon2D) does not get drawn at all. However its shader still somehow gets applied, as i get a nice gaussian blur. Furthermore, it seems to get drawn after its children, as there are no artifacts in my blur. I am not sure why it works this way but it does.
This is not a big problem in and of itself, as it works just fine. However, the way this code is currently set up prevents me from doing some things:
- I can not access the shader material in my script. If i try to access it in any way my second pass just breaks and I get an image that is only blurred in one direction This prevents me from scaling the blur radius (the parameter of grow()) automatically depending on sigma, hence why you have to set it manually.
- That also prevents me from scaling sigma depending on the size of the Polygon2D. This is something you will see in some of my other shaders, where I set the strength of the filter depending on the size of the covered area. This is essentially equivalent to the "Adapt pixel size" feature of the built-in pixelated filter of Hotscreen
- Even just introducing new variables to the Script that do absolutely nothing seem to break the code. This is absolutely beyond me and I am sure i must be doing something wrong or Hotscreen is just really fucked up. I have tested multiple variants of my code in the Godot editor, where they worked just fine, only to find out that they do not work in Hotscreen at all.

If you have made it this far there must be something wrong with you quite frankly. Who in their right mind would read all of that for a beta-censoring program?? I appreciate you nonetheless though and maybe you even learned something. If you know how to fix any of this mess, please let me know! I have tried many different approaches but maybe you know more than me. And even if you don't, I hope you have some "fun" with this silly little filter.

Stay censored, Betas!