Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a project setting to force an integer scale for window stretch #1666

Closed
Yukitty opened this issue Oct 14, 2020 · 36 comments · Fixed by godotengine/godot#75784
Closed

Add a project setting to force an integer scale for window stretch #1666

Yukitty opened this issue Oct 14, 2020 · 36 comments · Fixed by godotengine/godot#75784

Comments

@Yukitty
Copy link

Yukitty commented Oct 14, 2020

Describe the project you are working on:
Low resolution 2D pixel art games.

Describe the problem or limitation you are having in your project:
Base engine stretch modes can preserve aspect ratio, but do not attempt to guarantee an even and square pixel grid.

Describe the feature / enhancement and how it helps to overcome the problem or limitation:
New project setting checkbox located at display/window/stretch labelled "Force Integer Scale" (or similar) which changes stretch behavior to attempt preservation of square pixels.

Describe how your proposal will work, with code, pseudocode, mockups, and/or diagrams:
I've already produced the feature for 3.2.X here: Yukitty/godot@42af4d5
I am personally using this feature for all current and future 2D projects, as I do not produce or use high resolution art assets.

Ideal test project settings:

  • display/window/size/width and height set to something low, like 256x224 or 320x240
    (I am leaving display/window/size/test_width and test_height set to 1280x720 even in production)
  • display/window/size/resizable On.
  • display/window/stretch/mode 2d
  • Use pixel art assets (Filter OFF) and pixel snap to produce a scene.

Test fiddling with these settings:

  • display/window/stretch/integer_scale (the new feature)
  • display/window/stretch/aspect (different behaviors per setting)

Demonstrated behavior while Integer Scale is active:

  • If display/window/stretch/aspect is set to Keep, the game render will be letterboxed on ALL sides as the window expands, jumping up to the next available multiple of the base resolution only when both the window's width and height are adequate to support it.
  • If aspect is set to Keep Width or Keep Height, the game window will stretch vertically or horizontally, respectively, to almost fill the available space (discarding fractional pixels to preserve pixel accuracy). New letterbox bars will appear on the "kept" sides, which only jumps to resolution multiples.
  • If aspect is set to Expand, the game's aspect ratio will expand in both directions simultaneously, which is another entirely new behavior.
  • In all cases, if integer scale is checked, the pixels should be square, at all resolutions and window sizes, at all times. There may be minor rounding errors in my implementation, but the result is still notably better.

If this enhancement will not be used often, can it be worked around with a few lines of script?:
Almost. The primary blocker preventing a fully scripted solution from being viable is actually the font overdraw setting, which cannot be accessed outside of the SceneTree's root viewport stretch project settings, and is crucial for making dynamic fonts look good on a low resolution screen that's been scaled up.

Even avoiding that issue by using a pixel font, this behavior requires an obscure script and is not easily accessible to many users, as it's not a native setting of Viewports nor ViewportContainers that would display them.

There may be enough potential users to warrant a built-in simple setting, the same users of the Pixel Snap setting which currently work around this issue differently, by limiting the window size to specific game-enforced resolutions, or disabling the stretch feature entirely and writing it off as unusable.

Is there a reason why this should be core and not an add-on in the asset library?:
I can't currently fully replicate this behavior as an add-on due to the font overdraw, as described above.
Otherwise it may be viable, though it seems like an awfully minor feature to bury in the asset library. 🤔
It seems more suitably described as a base engine behavior rather than a key feature specific to any particular game or genre.

@Calinou
Copy link
Member

Calinou commented Oct 14, 2020

When working on godotengine/godot#21446, I thought about adding a "Stretch Force Integer Scale" checkbox which would constrain the final scaling ratio to the nearest integer factor. I would implement that in a separate pull request, still.

@Calinou Calinou changed the title Force integer scale setting for window stretch Add a project setting to force an integer scale setting for window stretch Oct 14, 2020
@Yukitty Yukitty changed the title Add a project setting to force an integer scale setting for window stretch Add a project setting to force an integer scale for window stretch Oct 14, 2020
@dalexeev
Copy link
Member

dalexeev commented Oct 14, 2020

1. For pixel games, it's better to use Viewport mode instead of 2D.

2. Here's my version of the script:

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

func _on_screen_resized():
    var screen = OS.get_screen_size() if OS.window_fullscreen else OS.window_size
    var k = min(int(screen.x / 320), int(screen.y / 180))
    var size = k * Vector2(320, 180)
    var pos = ((screen - size) / 2).floor()
    get_tree().root.set_attach_to_screen_rect(Rect2(pos, size))
    var end = screen - pos - size
    VisualServer.black_bars_set_margins(pos.x, pos.y, end.x, end.y)

3. If it will works out of the box (Pixel Perfect mode), it will be great. 😃

4. In addition, we also need the black_bars_bg_color and black_bars_bg_image settings.

@Yukitty
Copy link
Author

Yukitty commented Oct 15, 2020

Fine~ I attempted to make a fully-featured addon package as well.

The main differences being,

  1. since the plugin and/or autoload has to be enabled in order for the functionality to exist, there's no reason to include a separate checkbox to enable it
  2. for some reason I opted to add a separate resolution option instead of mis-using the "test resolution" setting again
  3. dynamic font overdraw doesn't work (obviously), but that's not a big loss.

So I suppose you can use that as a more accessible prototype implementation now too.
I do not believe this is a simple script to produce, and would still prefer a native setting, especially if more of the imperfect geometry can be fixed... 😓

I also have not gotten good results from the viewport scaling mode, counter-intuitively. It just makes the slight noise more noticeable.

Being able to turn the excessive letterboxing into a proper boarder as a color and/or texture would be neat, yes. 👍🏽 I think you can kind of do that currently with how I handle the "Expand" aspect ratio mode, but I didn't put a reasonable limit on how much it would expand if you pull the window ultra-widescreen or whatever...

@Yukitty
Copy link
Author

Yukitty commented Oct 15, 2020

Ohh, is it the physics/camera that are messing everything up now? Of course, only the individual polygons get snapped to pixels, the actual camera offset and object positions are still all over the place... 😓 Seems like this might take more work still.

dalexeev referenced this issue in Yukitty/godot-addon-integer_resolution_handler Oct 15, 2020
Version 1.0RC1 for Godot 3.2
@dalexeev
Copy link
Member

dalexeev commented Oct 15, 2020

@Yukitty I looked at your addon and left some comments. This is a very good prototype. 👍

The only thing, it seems to me, that this function should be implemented not by a checkbox, but as a separate stretch_mode (as a stricter version of the Viewport mode).

It doesn’t combine with disabled mode in any way, with 2d mode it is formally combined, but it looks like slyness.

Here's what the documentation says about it:

Stretch Mode = 2D: <...> This is a good option if your 2D artwork has a sufficiently high resolution and does not require pixel-perfect rendering.

2d mode adds new details for lines, fonts, etc.:

(That is, this cannot be called pixel perfect mode in any way. 🤣 )

This is "wrong" for pixel games, but okay. We can simply point this out in the documentation, but not take away the choice from users. However, the number of combinations of different modes, aspects and this new feature is truly impressive.

@Yukitty
Copy link
Author

Yukitty commented Oct 15, 2020

Yeah, I was just confused because viewport was looking mighty terrible, solely because my character and camera were moving on subpixels it turns out, and Pixel Snap by itself does not seem to 100% ensure things won't come up weird/inconsistent sometimes, especially at higher resolutions? 😓

I had to rewrite my whole player physics to work on a completely separate hidden layer with the sprites just following the whole-number positions around and a custom camera Node2D script doing the same...

@Calinou
Copy link
Member

Calinou commented Oct 15, 2020

@Yukitty This will be fixed by godotengine/godot#41535 if it's merged.

reduz added a commit to reduz/godot that referenced this issue Oct 30, 2020
-Rename pixel_snap to snap_2d_to_vertices
-Added snap_2d_to_transforms which is more useful

Fixes godotengine#41814
Solves proposal godotengine/godot-proposals#1666
Supersedes godotengine#35606, supersedes godotengine#41535, supersedes godotengine#41534
@realkotob
Copy link

realkotob commented Nov 17, 2020

I think this is closed by godotengine/godot#43194

@Calinou
Copy link
Member

Calinou commented Nov 17, 2020

@asheraryam No, this is a different issue. This proposal is about adding an option to constrain automatic viewport scaling to an integer factor.

@snoopdouglas
Copy link

snoopdouglas commented Nov 19, 2020

In case anyone has come across this and wants a visual explanation:

  • Here's a perfectly integer-scaled 2D scene (at 8 pixels to 1):

    Screenshot from 2020-11-19 20-12-29

  • Here's that same scene with the scale just slightly off an integer. Note the character's eyes:

    Screenshot from 2020-11-19 20-12-51

The latter is what this proposal is seeking to avoid. For me, this often happens when my game is in windowed mode and the window gets resized (away from a nice integer-scaled size).

@starry-abyss
Copy link

From my experience, if expanding viewport is used for pixel perfect scaling (that is, when the whole game window is covered, giving different view area for different aspect ratios) - it's desirable to have a callback where game developer can override integer factor for currently set window width/height.

This is because games have different metrics of what size of area the player needs to see. For example, in one of the games we used about 9 tiles per window height as ideal metric, and from all integer factors found the one to achieve the closest to this metric (as opposed to fixed view area with camera auto-stretched based on "as thin as possible black bars on sides metric").

@chucklepie
Copy link

chucklepie commented Jan 17, 2021

I can only confirm this is the case, on stretching my screen the result is truly awful :(

Every line is stretching the pixels wider or taller. Without any kind of simple solution I can't see how Godot can support 2D pixel games (which is the majority I think) while allowing the screen to be stretched (which is the majority I think).

image

@snoopdouglas
Copy link

I've just given 3.2.4-rc1 a go, and to be clear, it doesn't do what @Yukitty's addon does.

People landing here from Google, check the comment above.

@Calinou
Copy link
Member

Calinou commented Jan 28, 2021

I've just given 3.2.4-rc1 a go, and to be clear, it doesn't do what @Yukitty's addon does.

That's because we didn't add integer scaling yet 🙂

lawnjelly's pull requests were only concerned about improving the pixel snap behavior, not adding integer scaling.

@snoopdouglas
Copy link

Yes - just wanted to make it clear for anyone scanning the thread 👍

@chucklepie
Copy link

is there by any chance thought being applied to this functionality being added to Godot 4? the display/rendering/viewport/subviewport code has changed so much that existing projects like the ones described and created here as plugins do not work and require a massive amount of rework with very little documentation to go by...

Integer scaling is still very much useful :)

@Calinou
Copy link
Member

Calinou commented Jan 11, 2023

is there by any chance thought being applied to this functionality being added to Godot 4?

Not for 4.0, since we're in feature freeze now. godotengine/godot#63206 didn't get as much interest from contributors as I'd have hoped (it can't be merged as-is), so it was pushed for a future 4.x release.

Plugins will be updated eventually 🙂

@chucklepie
Copy link

chucklepie commented Jan 14, 2023

Integer snapping has been added to the camera I notice in g4, to go with pixel snapping. Clearly aimed at pixel platformers.

But lack of integer scaling on the viewpoint is just madness, if you have a pixel game you need integer scaling.

Sadly none of the current integer libraries work given the amount of changes to the render server, viewport, window, etc. I've tried them, and failed miserably to convert them.

I bet for a Godot maintainer it's a dozen lines of code with a checkbox in the project settings to front, but a million likes and appreciations 😉

@cybereality
Copy link

@chucklepie I know how to do integer scaling. I could make a plug-in, I was planning to at some point, but I mostly work in 3D so it was not useful to me. I think that's the best way forward right now, but if the plug-in gets interest I can speak with the team about adding to the Godot, maybe for 4.1.

@ssokolow
Copy link

ssokolow commented Jan 14, 2023

Integer snapping has been added to the camera I notice in g4, to go with pixel snapping. Clearly aimed at pixel platformers.

But lack of integer scaling on the viewpoint is just madness, if you have a pixel game you need integer scaling.

but I mostly work in 3D

Is integer scaling on the viewport only useful for pixel 2D stuff or would it also be relevant to certain kinds of retro-revival 3D games? (eg. 320x240 DOS 3D games, PSX/PS1/PSOne games, etc.)

@prominentdetail
Copy link

prominentdetail commented Jan 14, 2023

Is integer scaling on the viewport only useful for pixel 2D stuff or would it also be relevant to certain kinds of retro-revival 3D games? (eg. 320x240 DOS 3D games, PSX/PS1/PSOne games, etc.)

For my 3d game, I am using a resolution of 160x160 pixels.
I set the viewport size in Project Settings > Display > Window > Viewport Width/Height to 160 x 160
I set the Stretch mode to Viewport, and the Aspect to keep_height.

When I load the game, I do this is gdscript:

# setup window size
var screen_size = DisplayServer.screen_get_size()
var minSize = min(screen_size.x,screen_size.y)
DisplayServer.window_set_size( Vector2( floor(minSize/160)*160, floor(minSize/160)*160 ) )
var window_size = DisplayServer.window_get_size()
DisplayServer.window_set_position( screen_size*0.5 - window_size*0.5 )

That will create a window as large as it can at an integer scale (is the window is resized by user, then the pixels stretch and become non-integer),
and then based on any saved game settings decide if it should be fullscreen below:

if( global.settings.screen == 'fullscreen' ):
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
else:
	DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)

btw- I think this implementation will stretch it when going fullscreen, because it might not be 1 to 1 when changing from windowed size to fullscreen. Same if user manually drag the window corner and resizes it, the pixels become no longer integer based.

@prominentdetail
Copy link

prominentdetail commented Jan 14, 2023

The problem is that in order to have low res on 3d, you have to have the Stretch mode set to something other than disabled.
If it is set to Disabled, then you can't make the resolution smaller(It just uses max resolution of the window)..
But since you have to choose a Stretch mode, it stretches the pixels.. This is tied directly to the window size, and if the window size changes, the scale of the pixels within the window changes and becomes non-integer and irregular.

So yes, an integer option is also relevant to 3d, and would be useful to have. I don't know how to get that working for full screen applications or non integer sized windows, but hopefully that can be added.

@ssokolow
Copy link

It certainly sounds like it. I'm very much used to working in windowed mode and manually resizing windows and, when I trust applications enough to not try to change the display resolution and trick my window manager into crushing all my windows onto a single monitor, I still have a 1280x1024, 1920x1080, 1280x1024 spread... which means I don't have any 4:3 monitors for retro-authentic games to fullscreen onto without letterboxing or pillarboxing.

@chucklepie
Copy link

From the OP, this works fine in 3.x so just needs a massive overhaul because almost everything it does happens to be the major areas of change in Godot 4, however built in is the way to go, not a plugin
https://github.com/Yukitty/godot-addon-integer_resolution_handler/tree/3.X

@muno777
Copy link

muno777 commented Jan 17, 2023

This would be super helpful, especially since the removal of VisualServer.black_bars_set_margins() gets in the way of porting old solutions over to Godot 4 (e.g. https://github.com/Yukitty/godot-addon-integer_resolution_handler/tree/3.X relies on the removed black bar feature).

I'm looking forward to Calinou's PR being added to a future 4.x release!

@chucklepie
Copy link

chucklepie commented Jan 17, 2023 via email

@Calinou
Copy link
Member

Calinou commented Feb 25, 2023

Regarding plugins, it should be possible to do this using a custom viewport (and using the root viewport to display black bars). Given how much has changed in the windowing APIs, I recommend creating a new plugin from scratch rather than trying to port https://github.com/Yukitty/godot-addon-integer_resolution_handler to 4.0.

@chucklepie
Copy link

chucklepie commented Feb 26, 2023 via email

@Calinou
Copy link
Member

Calinou commented Feb 26, 2023

I've toyed around with a sharp bilinear filtering implementation for viewport scaling using the 2d stretch mode on the root viewport and a shader on a SubViewport node.

While this is not as sharp as integer scaling, it's still much sharper than plain bilinear filtering. This approach also works with any output resolution, including those that aren't a multiple of the base window size.

It should be possible to factor this into an easily reusable add-on (something that creates the subviewport for you, or provides it as an autoload). The same approach can also be used in 3.x.

Testing project (4.x): test_sharp_bilinear.zip

640×480 (base resolution)

Screenshot_20230226_214355

3840×2160 (4.5× scale factor)

Screenshot_20230226_214411

1105×786 (1.6375× scale factor)

Screenshot_20230226_214419

@fractalcounty
Copy link

As a newcomer to 4.0 and Godot as a whole, figuring out how to combine pixel perfect assets with a high resolution UI and smooth camera movement has been a nightmare.

There are dozens of factors that all impact this problem in various ways, such as:

  • Scaling mode in preferences
  • 2D vertices snap in preferences
  • 2D transform snap in preferences
  • Vertices/transform snap toggle in subviewport inspector
  • Enabling editor grid pixel snap to avoid placing objects at non-integer positions, which still seems to occasionally place them at subpixel positions anyways
  • Camera and player properties, i.e character and camera positions not being rounded to integers
  • Texture filtering method

This is a headache-inducing amount of variables to account for, and many of them are not well explained in the docs at the moment. Most common solutions for 3.0+ such as subpixel buffer shaders or using viewports don’t seem to intuitively translate to 4.0 despite my best efforts.

Whether this problem comes down to user error or could be improved upon within the editor is well beyond my scope. However, given that this is such a common use case, I think a set of best practices for pixel perfect games in the docs is desperately needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment