Pixel Perfect Scaling

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By Ellie93

So I’m trying to do pixel perfect scaling in definite increments (1x 2x 3x etc) in fullscreen.

I initially was handling this through the camera2d zoom function, but that doesn’t work as the pixel grid is not preserved when zooming

this post talks about what I’m trying to do: Pixel-perfect scaling mode · Issue #6506 · godotengine/godot · GitHub

but the code presented there is obsolete as it’s based on godot 2.1 function

The final message mentions that this is still doable in 3.0, through the viewport api, but doesn’t really explain how.

Can someone help me figure out how to do this kind of scaling in godot 3.0

:bust_in_silhouette: Reply From: chanon

A method that can be used to easily control the viewport pixel size is Viewport’s set_size_override method. You can use it to control the root viewport size without needing a special scene hierarchy.

For example, to make sure that the viewport doesn’t scale anything, set_size_override to the same as OS.get_window_size()

# maybe in an autoload/singleton but in a scene script should also work

func _ready():
	get_viewport().connect("size_changed", self, "window_resize")

func window_resize():
	get_viewport().set_size_override(true, OS.get_window_size())

Use this with Stretch mode 2d and Aspect ignore.

The size you send to set_size_override controls how many pixels the viewport takes from the source world_2d. In this case we set the number of pixels width/height that the viewport takes to be the same as the size of the window. Result is that no scaling/stretching occurs.

You will need to adjust the viewport’s global_canvas_transform so it is centered correctly on your content.

To do x2, x3 scaling if the window size is large enough, then just check the window size from get_window_size(). If it is large enough modify the size sent to set_size_override. (Eg. if it is >= 2x the size, then you can give setsizeoverride half of the window size.)

For your scenes, make sure your actual content is in the middle, and then surround it with whatever you want to show on the edges.

You can see Pixel-perfect scaling mode · Issue #6506 · godotengine/godot · GitHub for a full example.

chanon | 2018-03-14 17:34

I tried the code you posted on github, it does scale the content by integer but it still isn’t pixel perfect, as you can see in the image of my game scaled x2 the rotated bullets have visible pixels smaller than ships, and the lines I drew with draw_line() use smaller pixels than the sprites. Is there any way to use the scaling by integer while also preserving the original pixel size just like the viewport stretch setting?
enter image description here

nadomodan | 2018-03-28 21:48

:bust_in_silhouette: Reply From: sysharm

Unfortunately chanon’s script was made for 2d stretch mode which is not good for pixel perfect games. I adapted the scripts to use viewport stretch mode in Godot 3. Set the following script as an Autoload:

extends Node

# don't forget to use stretch mode 'viewport' and aspect 'ignore'
onready var viewport = get_viewport()

func _ready():
	get_tree().connect("screen_resized", self, "_screen_resized")

func _screen_resized():
	var window_size = OS.get_window_size()
	
	# see how big the window is compared to the viewport size
	# floor it so we only get round numbers (0, 1, 2, 3 ...)
	var scale_x = floor(window_size.x / viewport.size.x)
	var scale_y = floor(window_size.y / viewport.size.y)
	
	# use the smaller scale with 1x minimum scale
	var scale = max(1, min(scale_x, scale_y))
	
	# find the coordinate we will use to center the viewport inside the window
	var diff = window_size - (viewport.size * scale)
	var diffhalf = (diff * 0.5).floor()

	# attach the viewport to the rect we calculated
	viewport.set_attach_to_screen_rect(Rect2(diffhalf, viewport.size * scale))

Thank you for this. This post and script was just what i was looking for!

Pixel perfect makes my camera abit jaggy when moving diagonally, but thats another story… Will have to make some clever camera script now.

fossegutten | 2018-05-02 17:43

i’m new so not sure this would work but in game maker i would usually draw things (including camera locations) at rounded coordinates.
usually rounded to the nearest integer, but perhaps in godot u need to round by ur upscale factor?
note i wouldn’t actually change the location of the objects, just the location they draw at

decroded | 2018-09-19 14:37

Trying this with Godot 3.1 beta 2 and the script will never be called. Only when setting the scale mode to 2D. But not when using “VIEWPORT”. Breaking change in 3.1?

René Ruppert | 2019-01-18 16:54

I just wanted to mention that this script helped me enormously while I was working on my pixel-based game, it allowed the game to run at the highest res possible without overlapping past the bounds of the screen, and did its job well. The only issue was that it was locked into a predefined aspect ratio, so when I switched over to using an ultrawide for development I was left with a ton of unused space. For some games where a specific aspect ratio/size is a design choice, this isn’t a problem, but for a more standard game like mine it is.

So, I modified the script a bit; it still works the same, but now after it finds its largest scale, it extends the sides of the viewport by pixel perfect amounts just as much as is needed to fill the entire screen. Hope this helps! (Please let me know if there is a simpler way to achieve the same result)

extends Node

# don't forget to use stretch mode 'viewport' and aspect 'ignore'
onready var viewport = get_viewport()

func _ready():
	get_tree().connect("screen_resized", self, "_screen_resized")

func _screen_resized():
	var window_size = OS.get_window_size()
	
	# see how big the window is compared to the viewport size
	# floor it so we only get round numbers (0, 1, 2, 3 ...)
	var scale_x = floor(window_size.x / viewport.size.x)
	var scale_y = floor(window_size.y / viewport.size.y)
	
	# use the smaller scale with 1x minimum scale
	var scale = max(1, min(scale_x, scale_y))
	
	# extend the viewport to actually fit the screen as much as possible, in pixel perfect amounts
	#find the scaled size difference (basically visual pixel difference) between the screen and viewport dimensions
	var sizeDiff = window_size - (viewport.size * scale)
	var pixelDiff = (sizeDiff/scale).ceil()
	#If either dimension is odd, make it even by adding a pixel (otherwise you might have everything offset by a half pixel)
	if int(pixelDiff.x) % 2 == 1:
		pixelDiff.x += 1
	if int(pixelDiff.y) % 2 == 1:
		pixelDiff.y += 1
	#Now actually scale up the viewport to make it fill the screen
	viewport.set_size(viewport.size + pixelDiff)
	
	
	# find the coordinate we will use to center the viewport inside the window
	var diff = window_size - (viewport.size * scale)
	var diffhalf = (diff * 0.5).floor()
	
	# attach the viewport to the rect we calculated
	viewport.set_attach_to_screen_rect(Rect2(diffhalf, viewport.size * scale))

SKison | 2020-01-25 07:00

Thanks SKIson that works really well for me!

nbeardsworth | 2020-09-03 18:49

This works perfect, nice : )

samsface | 2020-12-21 16:59

:bust_in_silhouette: Reply From: exuin

Now that Godot 4 is out, viewports have been changed. I have written up new code for integer scaling here.

Make the scene tree like this:

Control
- ColorRect
- SubViewportContainer
  - SubViewport
    - Your game here!

Set the Control and the ColorRect’s anchors to Full Rect.

Here is the code on the root node.

extends Control


@onready var svc := $SVC


func _ready() -> void:
	get_viewport().size_changed.connect(on_screen_resized)
	svc.size = Vector2(ProjectSettings["display/window/size/viewport_width"], ProjectSettings["display/window/size/viewport_height"])
	on_screen_resized()
	
	
func on_screen_resized() -> void:
	var window_size := DisplayServer.window_get_size()
	var possible_scale := window_size / Vector2i(svc.size)
	var final_scale: Vector2i = max(1, min(possible_scale.x, possible_scale.y)) * Vector2i.ONE
	svc.scale = final_scale
	svc.position = Vector2(window_size) / 2 - svc.size * svc.scale / 2