extends Node enum ScaleMode { ## Stretches to fit as close as possible while maintaining the game's aspect ratio. ASPECT, ## Stretches to fit as close as possible while maintaining aspect ratio and an integer scale. INTEGER, ## Stretches to cover the whole screen, warping the aspect ratio. STRETCH, } ## Strategy to use for scaling from game-native resolution to the window/screen size. @export var scale_mode: ScaleMode = ScaleMode.ASPECT: set(value): scale_mode = value if is_node_ready(): _update_scale() ## Directory to scan screen filters from. @export_dir var filters_dir: String = "res://" @export_group("Internal References") @export var _viewport: SubViewport @export var _viewport_container: SubViewportContainer @export var _native_filters_layer: CanvasLayer ## The native resolution of the game. var size: Vector2i = Vector2i( ProjectSettings.get_setting("display/window/size/viewport_width"), ProjectSettings.get_setting("display/window/size/viewport_height"), ) ## Whether each filter is enabled or disabled. var filters_enabled: Dictionary[StringName, bool] = {} var _filter_instances: Dictionary[StringName, ColorRect] = {} func _enter_tree() -> void: get_tree().scene_changed.connect(_on_scene_changed) get_tree().root.size_changed.connect(_update_scale) # populate screen filters for file in ResourceLoader.list_directory(filters_dir): var material = load(filters_dir.path_join(file)) as Material if material: var id = StringName(file.get_basename()) if _filter_instances.has(id): push_error("Screen filter %s exists in two different resource files. Only one will exist" % id) _filter_instances[id].queue_free() _filter_instances.erase(id) var instance = ColorRect.new() instance.material = material if material.get_meta(&"filter_native_resolution", false): _native_filters_layer.add_child(instance) else: _viewport_container.add_child(instance) _filter_instances[id] = instance filters_enabled[id] = true _update_scale.call_deferred() var current_scene = get_tree().current_scene if current_scene and current_scene != self: current_scene.reparent.call_deferred(_viewport) func _on_scene_changed() -> void: for child in _viewport.get_children(): child.queue_free() get_tree().current_scene.reparent(_viewport) func _update_scale() -> void: var screen_size = Vector2(get_tree().root.size) var size_ratio = screen_size / Vector2(size) DisplayServer.window_set_min_size(size) _viewport.size = size _viewport_container.pivot_offset = Vector2(size) * 0.5 _viewport_container.position = screen_size * 0.5 - Vector2(size) * 0.5 match scale_mode: ScaleMode.ASPECT: # get the minimum ratio and use for both axes var min_scale = minf(size_ratio.x, size_ratio.y) _viewport_container.scale = Vector2(min_scale, min_scale) ScaleMode.INTEGER: # get the minimum ratio and use for both axes after flooring var min_scale = floorf(minf(size_ratio.x, size_ratio.y)) _viewport_container.scale = Vector2(min_scale, min_scale) ScaleMode.STRETCH: # just use the ratio as-is _viewport_container.scale = size_ratio # update screen filters state for filter in filters_enabled: _filter_instances[filter].visible = filters_enabled[filter] _filter_instances[filter].custom_minimum_size = Vector2(size)