godot ViewportTexture
workaround
In Godot 3 and 4, ViewportTexture
s do not have mipmaps. Unfortunately, if you’re like me you’re using the ViewportTexture
class as a way to render a UI in world space. Not having mipmaps more or less means that your UI looks really bad when it’s scaled due to aliasing artifacts.
The workaround for this problem is to increase the number of samples for each pixel that you want to render, just like a mipmap would do for you normally. In other words, we want to emulate what a mipmap would do in a shader.
Shader code is definitely not the most efficient way to do this. Alternative solutions include:
- Modifying the engine source so that
ViewportTexture
does generate mipmaps - Implementing a compute shader to generate the mipmaps on the GPU
Unfortunately, using the generate_mipmaps
function in the Image
class is too slow, because in order to get an Image
you have to copy the texture from the GPU to the CPU. I also don’t currently possess the knowledge to implement the other two solutions, so I’ve opted for the shader solution for now.
how it works
I’m going to assume that you already know what mipmaps are, but as a small reminder I would like you to remember that mipmaps improve image quality by providing a quick way to get the average color of a group of texels depending on how far we are zoomed out. In our shader, we can still get the average color of a group of texels every frame by doing what the mipmap would do for you. For example:
In this example, sample
lets us calculate what the color would be in the nth mipmap of albedo_texture
if it existed. It does this by doing what the mipmap would do; for the first mipmap, we average each 2x2 block of pixels, for the second we average each 4x4 block of pixels, and so on. Then, given the LOD value that we want to use, we linearly interpolate between the two closest mipmaps to get the resulting color.
However, this approach is slow. We have to do two passes in order to get the two color values to interpolate between (we could update the code to get the two color values in one pass, but I left it implemented the naive way to improve readability). The result is better than not doing this at all, but it also results in a rather blurry image when you view it from the side.
It’s possible to improve the image quality and speed of the shader by doing everything in a single pass. A friend, crazy_stewie
, made a single-pass solution which I’ve commented below:
In other words, instead of adhering closely to the concept of mipmaps, we can instead dynamically change how many texels we sample based on how far away from the texture we are.
Here’s what the viewport texture looks like before:
And after:
You also can’t see it from the picture, but the pixel shimmering effect you would normally get without this approach is also gone.