Pixel Perfect Font Resolution Scaling

Godot Version

4.6.stable

Question

Hello. I’d like to replicate how Ball x Pit manages its UI, i.e. it uses 2 themes, one for resolutions strictly under 1080p, and another one for 1080p and more. At least that’s how I understood it.

It seems to be impossible to do in Godot, I’ve spent the whole week on it.

Here’s what I would like to do :

  • the UI elements are drawn at 360p resolution, so that they can integer scale 2× or 3× for 720p and 1080p respectively.
  • at 720p, some texts use a pixel resolution of 1:1 and others 2:1, at 1080p, they respectively become 2:1 and 3:1.

Any help would be appreciated.

My current takeaways

  • upscaling from 360p would work wonderfully for the drawn UI elements, guaranteeing that what takes 2 pixels at 720p takes 3 at 1080p.
  • sadly, upscaling from 360p is out of the equation, because there is no way to triple the resolution and obtain text with a 2:1 resolution for 1080p.
  • hotswapping theme seems possible in order to scale the text sizes, but no idea how to affect the look of the UI elements.

I am building an app that adjust to the various resolutions of mobile devices. At startup, I copy a base theme and merge it with the default theme, then iterate through every relevant item that requires scaling (fonts, constants, icons, style-boxes, …).

Your base theme needs to include every Control type you need affected.

var _base_theme : Theme = ... # Contains _all_ the types you require
var _ui_scale : float = ... # your scaling factor

func init_theme():
    var default_theme = ThemeDB.get_default_theme()
    default_theme.merge_with(_base_theme)
    scale_theme(default_theme)
    default_theme.emit_changed()

func scale_theme(theme: Theme):
	const SCALABLES := [
		"separation", "h_separation", "v_separation",
		"margin_top", "margin_bottom", "margin_left", "margin_right"
	]

	var types: PackedStringArray = theme.get_type_list()
	for type_name: String in types:
		var font_sizes: PackedStringArray = theme.get_font_size_list(type_name)
		for font_name: String in font_sizes:
			if theme.has_font_size(font_name, type_name):
				var value: int = theme.get_font_size(font_name, type_name)
				var scaled: int = floori(value * _ui_scale)
				theme.set_font_size(font_name, type_name, scaled)

		var constants: PackedStringArray = theme.get_constant_list(type_name)
		for constant_name: String in constants:
			if constant_name in SCALABLES and theme.has_constant(constant_name, type_name):
				var constant_value: int = theme.get_constant(constant_name, type_name)
				var scaled_constant: int = floori(constant_value * _ui_scale)
				theme.set_constant(constant_name, type_name, scaled_constant)

		var styleboxes: PackedStringArray = theme.get_stylebox_list(type_name)
		for stylebox_name: String in styleboxes:
			if not theme.has_stylebox(stylebox_name, type_name):
				continue
			var stylebox: StyleBox = theme.get_stylebox(stylebox_name, type_name)
			if stylebox is StyleBoxFlat:
				var stylebox_flat: StyleBoxFlat = stylebox
				var scaled_stylebox: StyleBoxFlat = stylebox_flat.duplicate() as StyleBoxFlat
				scaled_stylebox.border_width_top = floori(stylebox_flat.border_width_top * _ui_scale)
				scaled_stylebox.border_width_bottom = floori(stylebox_flat.border_width_bottom * _ui_scale)
				scaled_stylebox.border_width_left = floori(stylebox_flat.border_width_left * _ui_scale)
				scaled_stylebox.border_width_right = floori(stylebox_flat.border_width_right * _ui_scale)
				scaled_stylebox.corner_radius_top_left = floori(stylebox_flat.corner_radius_top_left * _ui_scale)
				scaled_stylebox.corner_radius_top_right = floori(stylebox_flat.corner_radius_top_right * _ui_scale)
				scaled_stylebox.corner_radius_bottom_left = floori(stylebox_flat.corner_radius_bottom_left * _ui_scale)
				scaled_stylebox.corner_radius_bottom_right = floori(stylebox_flat.corner_radius_bottom_right * _ui_scale)
				scaled_stylebox.content_margin_top = stylebox_flat.content_margin_top * _ui_scale
				scaled_stylebox.content_margin_bottom = stylebox_flat.content_margin_bottom * _ui_scale
				scaled_stylebox.content_margin_left = stylebox_flat.content_margin_left * _ui_scale
				scaled_stylebox.content_margin_right = stylebox_flat.content_margin_right * _ui_scale
				scaled_stylebox.shadow_size = floori(stylebox_flat.shadow_size * _ui_scale)
				scaled_stylebox.shadow_offset = stylebox_flat.shadow_offset * _ui_scale
				theme.set_stylebox(stylebox_name, type_name, scaled_stylebox)

		var icons: PackedStringArray = theme.get_icon_list(type_name)
		for icon_name: String in icons:
			if theme.has_icon(icon_name, type_name):
				var image: Texture2D = get_icon(icon_name)
				if image != null:
					theme.set_icon(icon_name, type_name, image)