Add resolution-independent pixelation

This commit is contained in:
Alexander Sellite 2026-07-27 23:45:11 -04:00
parent 46175ee405
commit 6f51f91324
10 changed files with 220 additions and 33 deletions

View file

@ -30,6 +30,9 @@ const UIPixelationPresenterType = preload(
const PixelationResetOverlayType = preload( const PixelationResetOverlayType = preload(
"res://ui/pixelation_reset_overlay.gd" "res://ui/pixelation_reset_overlay.gd"
) )
const WorldPixelationPostprocessType = preload(
"res://main/world_pixelation_postprocess.gd"
)
const TITLE_MUSIC_SILENCE_DB: float = -80.0 const TITLE_MUSIC_SILENCE_DB: float = -80.0
@ -53,6 +56,9 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
@onready var _pixelation_reset: PixelationResetOverlayType = ( @onready var _pixelation_reset: PixelationResetOverlayType = (
%PixelationResetOverlay %PixelationResetOverlay
) )
@onready var _world_pixelation: WorldPixelationPostprocessType = (
%WorldPixelationPostprocess
)
var _gameplay_started: bool = false var _gameplay_started: bool = false
var _shop_interaction: FishingShopInteractionType var _shop_interaction: FishingShopInteractionType
@ -241,14 +247,7 @@ func _process(_delta: float) -> void:
func _apply_runtime_settings(settings: PlayerSettingsType) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
if settings == null: if settings == null:
return return
var root_viewport: Viewport = get_viewport() _apply_world_pixelation(settings.world_pixel_size)
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
root_viewport.scaling_3d_scale = PlayerSettingsType.get_world_render_scale(
settings.world_pixel_size
)
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
root_viewport.use_taa = false
_ui_pixelation.set_pixel_size(settings.ui_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size)
_game_ui.get_title_screen().set_world_pixelation( _game_ui.get_title_screen().set_world_pixelation(
settings.world_pixel_size settings.world_pixel_size
@ -264,6 +263,16 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
) )
func _apply_world_pixelation(pixel_size: int) -> void:
var root_viewport: Viewport = get_viewport()
root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST
root_viewport.scaling_3d_scale = 1.0
root_viewport.msaa_3d = Viewport.MSAA_DISABLED
root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
root_viewport.use_taa = false
_world_pixelation.set_pixel_size(pixel_size)
func _on_effective_ui_pixel_size_changed( func _on_effective_ui_pixel_size_changed(
_requested_pixel_size: int, _requested_pixel_size: int,
effective_pixel_size: int, effective_pixel_size: int,
@ -281,6 +290,7 @@ func _reset_pixelation() -> void:
func _set_gameplay_active(active: bool) -> void: func _set_gameplay_active(active: bool) -> void:
_gameplay_started = active _gameplay_started = active
_world_pixelation.set_gameplay_active(active)
_ui_pixelation.set_gameplay_active(active) _ui_pixelation.set_gameplay_active(active)
if not active: if not active:
_player.item_effects.reset_all() _player.item_effects.reset_all()

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=16 format=3] [gd_scene load_steps=17 format=3]
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"] [ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] [ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
@ -15,6 +15,7 @@
[ext_resource type="AudioStream" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"] [ext_resource type="AudioStream" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"]
[ext_resource type="Script" path="res://ui/ui_pixelation_presenter.gd" id="14_pixelation"] [ext_resource type="Script" path="res://ui/ui_pixelation_presenter.gd" id="14_pixelation"]
[ext_resource type="PackedScene" path="res://ui/pixelation_reset_overlay.tscn" id="15_reset"] [ext_resource type="PackedScene" path="res://ui/pixelation_reset_overlay.tscn" id="15_reset"]
[ext_resource type="PackedScene" path="res://main/world_pixelation_postprocess.tscn" id="16_world_pixelation"]
[node name="Main" type="Node3D"] [node name="Main" type="Node3D"]
script = ExtResource("3_main") script = ExtResource("3_main")
@ -53,6 +54,9 @@ stream = ExtResource("13_title_music")
volume_db = -80.0 volume_db = -80.0
bus = &"Music" bus = &"Music"
[node name="WorldPixelationPostprocess" parent="." instance=ExtResource("16_world_pixelation")]
unique_name_in_owner = true
[node name="UIPresentation" type="SubViewportContainer" parent="."] [node name="UIPresentation" type="SubViewportContainer" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
texture_filter = 1 texture_filter = 1

View file

@ -0,0 +1,51 @@
class_name WorldPixelationPostprocess
extends CanvasLayer
@onready var _screen_grid: ColorRect = %ScreenGrid
var _pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE
var _gameplay_active: bool = false
func _ready() -> void:
get_viewport().size_changed.connect(_refresh_grid)
_refresh_grid()
func set_pixel_size(pixel_size: int) -> void:
_pixel_size = clampi(
pixel_size,
PlayerSettings.MIN_WORLD_PIXEL_SIZE,
PlayerSettings.MAX_WORLD_PIXEL_SIZE
)
_refresh_grid()
func set_gameplay_active(active: bool) -> void:
_gameplay_active = active
_refresh_grid()
func get_grid_size() -> Vector2i:
return PlayerSettings.get_world_grid_size(
_pixel_size,
get_window().size
)
func _refresh_grid() -> void:
if not is_node_ready():
return
var effect_enabled: bool = (
_gameplay_active
and _pixel_size != PlayerSettings.MIN_WORLD_PIXEL_SIZE
)
_screen_grid.visible = effect_enabled
if not effect_enabled:
return
var shader_material := _screen_grid.material as ShaderMaterial
if shader_material != null:
shader_material.set_shader_parameter(
"logical_grid_size",
Vector2(get_grid_size())
)

View file

@ -0,0 +1 @@
uid://c3n47x8egoc8x

View file

@ -0,0 +1,15 @@
shader_type canvas_item;
render_mode unshaded;
uniform sampler2D screen_texture
: hint_screen_texture, filter_nearest, repeat_disable;
uniform vec2 logical_grid_size = vec2(640.0, 360.0);
void fragment() {
vec2 safe_grid = max(logical_grid_size, vec2(1.0));
vec2 snapped_uv = (
floor(SCREEN_UV * safe_grid) + vec2(0.5)
) / safe_grid;
COLOR = texture(screen_texture, snapped_uv);
}

View file

@ -0,0 +1 @@
uid://sq224d8sn1b4

View file

@ -0,0 +1,21 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://main/world_pixelation_postprocess.gd" id="1_script"]
[ext_resource type="Shader" path="res://main/world_pixelation_postprocess.gdshader" id="2_shader"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_world_pixelation"]
shader = ExtResource("2_shader")
[node name="WorldPixelationPostprocess" type="CanvasLayer"]
layer = -1
script = ExtResource("1_script")
[node name="ScreenGrid" type="ColorRect" parent="."]
unique_name_in_owner = true
material = SubResource("ShaderMaterial_world_pixelation")
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2

View file

@ -13,8 +13,11 @@ const DEFAULT_WORLD_PIXEL_SIZE: int = 3
const MIN_UI_PIXEL_SIZE: int = 1 const MIN_UI_PIXEL_SIZE: int = 1
const MAX_UI_PIXEL_SIZE: int = 5 const MAX_UI_PIXEL_SIZE: int = 5
const DEFAULT_UI_PIXEL_SIZE: int = 3 const DEFAULT_UI_PIXEL_SIZE: int = 3
const WORLD_RENDER_SCALES: Array[float] = [1.0, 0.9, 0.8, 0.65, 0.45] const COMPACT_DISPLAY_HEIGHT: int = 560
const UI_RENDER_SCALES: Array[float] = [1.0, 0.975, 0.95, 0.925, 0.9] const WORLD_DESKTOP_GRID_HEIGHTS: Array[int] = [0, 540, 360, 216, 96]
const WORLD_COMPACT_GRID_HEIGHTS: Array[int] = [0, 360, 240, 144, 72]
const UI_DESKTOP_RENDER_HEIGHTS: Array[int] = [0, 612, 504, 396, 288]
const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
@export var auto_click_enabled: bool = false @export var auto_click_enabled: bool = false
@export_range(0.10, 0.50, 0.01) var auto_click_interval: float = 0.20 @export_range(0.10, 0.50, 0.01) var auto_click_interval: float = 0.20
@ -55,19 +58,89 @@ func copy() -> PlayerSettings:
return result return result
static func get_world_render_scale(pixel_size: int) -> float: static func get_world_grid_height(
pixel_size: int,
displayed_height: int,
) -> int:
var index: int = clampi( var index: int = clampi(
pixel_size, pixel_size,
MIN_WORLD_PIXEL_SIZE, MIN_WORLD_PIXEL_SIZE,
MAX_WORLD_PIXEL_SIZE MAX_WORLD_PIXEL_SIZE
) - 1 ) - 1
return WORLD_RENDER_SCALES[index] if index == 0:
return maxi(1, displayed_height)
var targets: Array[int] = (
WORLD_COMPACT_GRID_HEIGHTS
if displayed_height < COMPACT_DISPLAY_HEIGHT
else WORLD_DESKTOP_GRID_HEIGHTS
)
return mini(maxi(1, displayed_height), targets[index])
static func get_ui_render_scale(pixel_size: int) -> float: static func get_world_grid_size(
pixel_size: int,
displayed_size: Vector2i,
) -> Vector2i:
var safe_size := Vector2i(
maxi(1, displayed_size.x),
maxi(1, displayed_size.y)
)
var target_height: int = get_world_grid_height(
pixel_size,
safe_size.y
)
return Vector2i(
maxi(
1,
roundi(
float(target_height)
* float(safe_size.x)
/ float(safe_size.y)
)
),
target_height
)
static func get_ui_render_height(
pixel_size: int,
displayed_height: int,
) -> int:
var index: int = clampi( var index: int = clampi(
pixel_size, pixel_size,
MIN_UI_PIXEL_SIZE, MIN_UI_PIXEL_SIZE,
MAX_UI_PIXEL_SIZE MAX_UI_PIXEL_SIZE
) - 1 ) - 1
return UI_RENDER_SCALES[index] if index == 0:
return maxi(1, displayed_height)
var targets: Array[int] = (
UI_COMPACT_RENDER_HEIGHTS
if displayed_height < COMPACT_DISPLAY_HEIGHT
else UI_DESKTOP_RENDER_HEIGHTS
)
return mini(maxi(1, displayed_height), targets[index])
static func get_ui_render_size(
pixel_size: int,
displayed_size: Vector2i,
) -> Vector2i:
var safe_size := Vector2i(
maxi(1, displayed_size.x),
maxi(1, displayed_size.y)
)
var target_height: int = get_ui_render_height(
pixel_size,
safe_size.y
)
return Vector2i(
maxi(
1,
roundi(
float(target_height)
* float(safe_size.x)
/ float(safe_size.y)
)
),
target_height
)

View file

@ -379,20 +379,25 @@ func set_world_pixelation(pixel_size: int) -> void:
func _update_world_preview_resolution() -> void: func _update_world_preview_resolution() -> void:
var render_scale: float = PlayerSettings.get_world_render_scale( var displayed_size := Vector2i(
_world_pixel_size maxi(1, roundi(size.x)),
maxi(1, roundi(size.y))
)
var grid_size: Vector2i = PlayerSettings.get_world_grid_size(
_world_pixel_size,
displayed_size
) )
var shader_material := _background.material as ShaderMaterial var shader_material := _background.material as ShaderMaterial
if shader_material != null: if shader_material != null:
shader_material.set_shader_parameter( shader_material.set_shader_parameter(
"virtual_pixel_density", "virtual_pixel_density",
Vector2( Vector2(grid_size)
maxf(1.0, size.x * render_scale),
maxf(1.0, size.y * render_scale)
)
) )
var logo_material := _title_logo.material as ShaderMaterial var logo_material := _title_logo.material as ShaderMaterial
if logo_material != null: if logo_material != null:
var render_scale: float = (
float(grid_size.y) / float(displayed_size.y)
)
var effect_scale: float = 1.0 / render_scale var effect_scale: float = 1.0 / render_scale
logo_material.set_shader_parameter( logo_material.set_shader_parameter(
"horizontal_displacement_pixels", "horizontal_displacement_pixels",
@ -405,9 +410,15 @@ func _update_world_preview_resolution() -> void:
func _snap_world_preview(value: Vector2) -> Vector2: func _snap_world_preview(value: Vector2) -> Vector2:
var render_scale: float = PlayerSettings.get_world_render_scale( var displayed_size := Vector2i(
_world_pixel_size maxi(1, roundi(size.x)),
maxi(1, roundi(size.y))
) )
var grid_size: Vector2i = PlayerSettings.get_world_grid_size(
_world_pixel_size,
displayed_size
)
var render_scale: float = float(grid_size.y) / float(displayed_size.y)
var step: float = 1.0 / render_scale var step: float = 1.0 / render_scale
return Vector2( return Vector2(
roundf(value.x / step) * step, roundf(value.x / step) * step,

View file

@ -1,7 +1,7 @@
class_name UIPixelationPresenter class_name UIPixelationPresenter
extends SubViewportContainer extends SubViewportContainer
const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(320, 180) const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(256, 180)
signal effective_pixel_size_changed( signal effective_pixel_size_changed(
requested_pixel_size: int, requested_pixel_size: int,
@ -70,12 +70,13 @@ func _resize_presentation() -> void:
return return
var previous_effective_size: int = _effective_pixel_size var previous_effective_size: int = _effective_pixel_size
_effective_pixel_size = _resolve_effective_pixel_size(root_size) _effective_pixel_size = _resolve_effective_pixel_size(root_size)
var render_scale: float = PlayerSettings.get_ui_render_scale( var viewport_size: Vector2i = PlayerSettings.get_ui_render_size(
_effective_pixel_size _effective_pixel_size,
root_size
) )
var viewport_size := Vector2i( var render_scale: float = minf(
ceili(float(root_size.x) * render_scale), 1.0,
ceili(float(root_size.y) * render_scale) float(viewport_size.y) / float(root_size.y)
) )
var presentation_size: Vector2 = Vector2(viewport_size) / render_scale var presentation_size: Vector2 = Vector2(viewport_size) / render_scale
set_anchors_preset(Control.PRESET_TOP_LEFT) set_anchors_preset(Control.PRESET_TOP_LEFT)
@ -98,10 +99,9 @@ func _resize_presentation() -> void:
func _resolve_effective_pixel_size(root_size: Vector2i) -> int: func _resolve_effective_pixel_size(root_size: Vector2i) -> int:
for candidate: int in range(_requested_pixel_size, 0, -1): for candidate: int in range(_requested_pixel_size, 0, -1):
var render_scale: float = PlayerSettings.get_ui_render_scale(candidate) var candidate_size: Vector2i = PlayerSettings.get_ui_render_size(
var candidate_size := Vector2i( candidate,
ceili(float(root_size.x) * render_scale), root_size
ceili(float(root_size.y) * render_scale)
) )
if ( if (
candidate_size.x >= MIN_UI_VIEWPORT_SIZE.x candidate_size.x >= MIN_UI_VIEWPORT_SIZE.x