diff --git a/assets/screen_filters/lcd.tres b/assets/screen_filters/lcd.tres new file mode 100644 index 0000000..3ea3eba --- /dev/null +++ b/assets/screen_filters/lcd.tres @@ -0,0 +1,11 @@ +[gd_resource type="ShaderMaterial" load_steps=2 format=3 uid="uid://sppw51urwmf3"] + +[ext_resource type="Shader" uid="uid://bgy86nka8c76n" path="res://assets/shaders/lcd.gdshader" id="1_y1c64"] + +[resource] +shader = ExtResource("1_y1c64") +shader_parameter/enabled = true +shader_parameter/resolution = Vector2(256, 192) +shader_parameter/curvature = Vector2(1, 1) +shader_parameter/scanline_opacity = Vector2(0.1, 0.1) +shader_parameter/brightness = 1.0 diff --git a/assets/shaders/lcd.gdshader b/assets/shaders/lcd.gdshader new file mode 100644 index 0000000..2a3ae2c --- /dev/null +++ b/assets/shaders/lcd.gdshader @@ -0,0 +1,43 @@ +shader_type canvas_item; + +uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; + +uniform bool enabled = true; +uniform vec2 resolution; +uniform vec2 curvature; +uniform vec2 scanline_opacity; +uniform float brightness; + +vec2 curve_uv(vec2 uv) { + uv = uv * 2.0 - 1.0; + vec2 offset = abs(uv.yx) / vec2(curvature.x, curvature.y); + uv = uv + uv * offset * offset; + uv = uv * 0.5 + 0.5; + return uv; +} + +vec3 scanline_intensity(float uv, float res, float opacity) { + float intensity = sin((uv * res - 0.25) * PI * 2.0); + intensity = ((0.5 * intensity) + 0.5) * 0.9 + 0.1; + return vec3(pow(intensity, opacity)); +} + +void fragment() { + if (enabled) { + vec2 curved_uv = SCREEN_UV; + vec3 base_color = texture(SCREEN_TEXTURE, curved_uv).rgb; + + base_color += vec3(1.0/256.0); + base_color *= scanline_intensity(curved_uv.x, resolution.x, scanline_opacity.y); + base_color *= scanline_intensity(curved_uv.y, resolution.y, scanline_opacity.x); + base_color *= vec3(brightness); + + if (curved_uv.x < 0.0 || curved_uv.y < 0.0 || curved_uv.x > 1.0 || curved_uv.y > 1.0) { + COLOR = vec4(0.0, 0.0, 0.0, 0.0); + } else { + COLOR = vec4(base_color, 1.0); + } + } else { + COLOR = texture(SCREEN_TEXTURE, SCREEN_UV); + } +} \ No newline at end of file diff --git a/assets/shaders/lcd.gdshader.uid b/assets/shaders/lcd.gdshader.uid new file mode 100644 index 0000000..8bac002 --- /dev/null +++ b/assets/shaders/lcd.gdshader.uid @@ -0,0 +1 @@ +uid://bgy86nka8c76n diff --git a/autoloads/display.gd b/autoloads/display.gd index 3252b54..5008f36 100644 --- a/autoloads/display.gd +++ b/autoloads/display.gd @@ -18,9 +18,13 @@ enum ScaleMode { 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. @@ -29,11 +33,37 @@ var size: Vector2i = Vector2i( 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 @@ -69,3 +99,8 @@ func _update_scale() -> void: 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) diff --git a/autoloads/display.tscn b/autoloads/display.tscn index 1923fdd..0a37dbc 100644 --- a/autoloads/display.tscn +++ b/autoloads/display.tscn @@ -2,11 +2,12 @@ [ext_resource type="Script" uid="uid://kwa8v1dhwlie" path="res://autoloads/display.gd" id="1_1seuv"] -[node name="Display" type="Node" node_paths=PackedStringArray("_viewport", "_viewport_container")] +[node name="Display" type="Node" node_paths=PackedStringArray("_viewport", "_viewport_container", "_native_filters_layer")] script = ExtResource("1_1seuv") -scale_mode = 1 +filters_dir = "res://assets/screen_filters" _viewport = NodePath("SubViewportContainer/SubViewport") _viewport_container = NodePath("SubViewportContainer") +_native_filters_layer = NodePath("SubViewportContainer/SubViewport/NativeResolutionFilters") [node name="SubViewportContainer" type="SubViewportContainer" parent="."] anchors_preset = 8 @@ -26,3 +27,6 @@ pivot_offset = Vector2(128, 96) handle_input_locally = false size = Vector2i(256, 192) render_target_update_mode = 4 + +[node name="NativeResolutionFilters" type="CanvasLayer" parent="SubViewportContainer/SubViewport"] +layer = 2147483647 diff --git a/project.godot b/project.godot index 0c922b5..f079670 100644 --- a/project.godot +++ b/project.godot @@ -11,6 +11,7 @@ config_version=5 [application] config/name="Luna Lunatic" +run/main_scene="uid://bnjujh22nqr4a" config/features=PackedStringArray("4.5", "GL Compatibility") config/icon="res://icon.svg" @@ -26,6 +27,7 @@ window/size/viewport_height=192 [file_customization] folder_colors={ +"res://assets/": "yellow", "res://autoloads/": "orange" }