Hello,

I've been trying to apply viewport scaling to a 2D scene to achieve supersampling (which I want to use as antialiasing, I made some posts on that topic previously). I started from this demo: https://godotengine.org/asset-library/asset/586

and while it seems to work fine for 3D, I can't find a way to apply it to 2D. As soon the viewport scale change, 2D object move and around - so instead of rendering the viewport at desired scale and then rescale to the main viewport size (thats how I understand it should work) it just resizes the viewport and renders as-is.

How can I get it to work like the 3D one?

Or, if someone has any other idea how to make a rectangular Polygon2D able to rotate and not have jagged edges that works on all platforms, I'd be incredible grateful if you would share it. The filtering texture method thats in the docs doesnt work for me as I need all sides of it to be filtered. I've spend several weeks now on this and I'm starting to consider throwing months of work away and start again in a different engine just because of this... It's vital for my game but I just can't get it to work, and apart from that I really like Godot and the way it works.

ViewportContainer can also be used to display a viewport in a GUI, but it doesn't offer the ability to enable filtering.

I guess the first question to ask is what are you displaying your viewport in?

@Nanites said: The filtering texture method thats in the docs doesnt work for me as I need all sides of it to be filtered.

Wait, so it works but the issue is the border regions? How about rendering at a higher resolution and just leaving the surrounding bits out of view?

I first tried to just add 2D elements to the demo (which has Control->ViewportContainer->Viewport) and also adding a ViewportContainer->Viewport to my games main scene which has a Node2D as root.

You can do it with this code globally, no need for viewports (it actually uses the main viewport, which is the root node in Godot and always exists). This allows you to switch to any resolution you want (for it to work, the game as to be running at native res, for example with stretch mode 2D and aspect keep height).

extends Node2D

const RES_SD = Vector2(854, 480)
const RES_HD = Vector2(1280, 720)
const RES_FHD = Vector2(1920, 1080)
const RES_QHD = Vector2(2560, 1440)
const RES_UHD = Vector2(3840, 2160)
enum RESOLUTION { SD, HD, FHD, QHD, UHD }
export (RESOLUTION) var resolution := RESOLUTION.SD setget update_resolution
var viewport_size

func _ready():
	get_viewport().connect("size_changed", self, "on_window_resize")
	set_viewport_size()
	on_window_resize()
	
func set_viewport_size():
	var view_res = RES_SD
	match resolution:
		RESOLUTION.SD:
			view_res = RES_SD
		RESOLUTION.HD:
			view_res = RES_HD
		RESOLUTION.FHD:
			view_res = RES_FHD
		RESOLUTION.QHD:
			view_res = RES_QHD
		RESOLUTION.UHD:
			view_res = RES_UHD
	viewport_size = view_res
	
func update_resolution(res):
	resolution = res
	set_viewport_size()
	on_window_resize()

func on_window_resize():
	var viewport = get_viewport()
	if viewport:
		viewport.size = viewport_size

Just place that on a Node2D anywhere in your main scene and you can adjust the resolution with the inspector. It works well for going lower than native (for example if you want to increase performance in a 3D game). It does work in 2D but it can introduce shimmering on textures when the resolution is high (for example 4K res on a 1080p screen). If you enable FXAA in the project settings it helps a bit but does not eliminate the shimmering.

Another option is to use higher quality images. The sprite size and the image size do not have to be the same. For example, if the sprite is 64 x 128, make the image 128 x 256 and just scale the sprite down to 50%. This will be the same size, but with the advantage of additional filtering, which results in a higher quality image (and this is necessary with my script if you want to go to 4K).

@cybereality said: It works well for going lower than native

I tried it with my scene and it seems that going higher does not work very well - I set it to 4x native resolution but everything is rendered very jagged, like the actual resolution is actually lower then native, not higher.

Also I forgot to mention it: my game doesnt not have textures or sprites. I guess you could call it "minimal style", just solid colored polygons. Which has the downside of their edges, if not aliased properly, standing out and looking extremely bad...

The resolution is higher, but I don't think it supports supersampling, meaning it will be taking the same samples as a native resolution image (the viewport texture itself is being scaled up properly, but sampled wrong, meaning there are pixels missing resulting in jagged edges). I believe this is fixed in Godot 4.0, I saw a video somewhere of it and it looks good.

I think the only way to fix this right now is to write a supersampling full screen shader yourself. You can use that code I posted as a base. But then send the result of the scaled viewport to a full screen shader that will read multiple samples per pixel (probably 4 samples is enough, poisson disk sampling is a good method) and then write out to the screen. I'm busy with some stuff now, but I don't think this would be hard to do. I can look into it later.

If you could help me with that, I would incredible appreciate this, as it will probably be quite a bit above what I can do currently. My game pretty much depends on solving this issue, that will make or break it as without AA it is just incredible terrible to look at.

I'll try it out myself in the meantime but I just lack too much understanding so far of how shaders even work... Your help would be incredibly appreciated.

Something to note, you might have to account for the supersampling in your sprites positions.

In 3D, the positions are calculated in world position, whilst 2D might be calculated differently. I might be wrong on that, but that's my take on why everything might misalign between the normal viewport and the supersampled viewport.

I had a similar problem when trying to scale objects in 3D on a grid-based system and it worked fine when including the size value in the calculation.

Just thought I would try and help a little even though I am far from good with 2D on Godot :) Hopefully this was of some help. Wishing you and yours a wonderful day!

When performing supersampling, there is no point in going above a rendering scale factor of 2.0 because the viewport won't be displayed with mipmaps. A scale factor of 2.0 provides 4× SSAA already (2 times resolution on each axis), and it's expensive enough already :)

My game pretty much depends on solving this issue, that will make or break it as without AA it is just incredible terrible to look at.

Have you tried drawing several identical lines/polygons with lower opacity, but with slight pixel offsets? This works fairly well in my experience. I recently implemented this in another engine: https://github.com/coelckers/gzdoom/pull/1516

@Calinou said: When performing supersampling, there is no point in going above a rendering scale factor of 2.0 because the viewport won't be displayed with mipmaps. A scale factor of 2.0 provides 4× SSAA already (2 times resolution on each axis), and it's expensive enough already :)

My game pretty much depends on solving this issue, that will make or break it as without AA it is just incredible terrible to look at.

Have you tried drawing several identical lines/polygons with lower opacity, but with slight pixel offsets? This works fairly well in my experience. I recently implemented this in another engine: https://github.com/coelckers/gzdoom/pull/1516

Simply out of curiosity, does LOD work on 2D? That could also be an option that would also possibly save on performances, even if the impact is small, by only applying the supersampling effect to the specific sprites. You could use the LOD to have for example the default sprite be at lets say 64x64, then a better LOD at 128x128 (for a x2 effect.), 256x256 (for a x4 effect) and so on. This could also allow for older computers to run the game by using a crappier LOD such as 32x32 and so on.

It might take a little more work as you would need to have a high quality sprite that you'd need to scale down whilst keeping the pixels correct, but that could be a nice way to save performances to the cost of a little more disk space when exporting.

It would also allow the code and setup to remain the same with a few adjustments to include the switching of those sprites.

Otherwise, if LOD isn't in the 2D part of the engine, it could still be implemented rather easily.

Okay, I started looking into it and realized it might be helpful in my demo, so I'm going to try to get it working. I have some code that looks like it works, but I need to alter the viewport setup in Godot, as it requires two viewports. I should be able to get it working later today.

@Calinou said: Have you tried drawing several identical lines/polygons with lower opacity, but with slight pixel offsets? This works fairly well in my experience. I recently implemented this in another engine: https://github.com/coelckers/gzdoom/pull/1516

I have tried something similar, but I could never get it to work in a satisfactory way with the sharp contrasts between the colors in my game, and the fact that my polygons can move around, and if two of them touch each other no seam should be visible.

@cybereality said: Okay, I started looking into it and realized it might be helpful in my demo, so I'm going to try to get it working. I have some code that looks like it works, but I need to alter the viewport setup in Godot, as it requires two viewports. I should be able to get it working later today.

This sounds great, thank you a lot! :)

Okay, so I did get it working, however it is way too slow. Here is how it looks:

That is a 1440p render supersampled down to 720p. The quality is amazing, unfortunately it is running at 25 fps on a 720p window with a simple scene. If you go fullscreen (1440p on my monitor) it renders to 5K and that is 5 fps (but it does look great). I think the problem is reading the viewport texture on the CPU, which is costly. I'm going to keep looking into it and see if there is a way to do it fully on the GPU.

I did get it working. It was really easy, not sure if anyone figured this out yet cause all the tutorials I found were wrong. But it does work, I'm getting 3000 fps at 5K!!! (yes 3,000 at 5120x2880 resolution). I need to clean a few things up, give me an hour or two and I'll post the code.

Here is the project file. It works perfectly. It should be clear how it works, but let me know if you have questions. Also, you have to use GLES3 as the shader function requires it. If you need GLES2 I'd need to rewrite the function myself, but I think that would hurt performance.

@cybereality

Thank you a lot!!

This works fantastic, exactly how I tried to get it.

I recently switched to GLES2, as per your recommendation here: https://godotforums.org/discussion/comment/57167/#Comment_57167 since I want to support as many devices as possible, including old ones (my game is easy on the hardware anyway). So if you could make a GLES2 version, that would be great. Also I'd like to thank you in the credits of my game when I release it, without your help I'd have a major issue here. =)

Also, I think this project would be a great addition for https://github.com/godotengine/godot-demo-projects/ While I tried to find a solution for this, there were many posts of people over the years trying to do the same but no real solution. This one is simple, works great and should help a lot of people!

Okay nice! Yeah, I can make a GLES2 version tomorrow probably. The method was a little complex, but it's actually not a lot of code. I'm surprised no one figured this out already. I think the github is for official projects, but I will contact them about adding it. I also plan to submit to the AssetLib but I still need to clean up a few things before it is ready. I'm really happy it worked.

Here's the GLES2 version. I had to implement some shader functions myself but the result is pretty close (if not the same). I think that might be slightly slower than using the hardware functions, however, since GLES2 now works, the performance overall is much better (about 50% better). I also changed it so you don't have to make the world a child of the Resolution node. You can work on your game as normal and it handles everything.

Thank you again a lot! :)

I'll try it out as soon I'm home again.