├── addons └── smooth_pixel_subviewport_container │ ├── LICENSE.txt │ ├── README.md │ ├── SmoothPixelSubViewportContainer.svg │ ├── SmoothPixelSubViewportContainer.svg.import │ ├── plugin.cfg │ ├── plugin.gd │ ├── smooth_pixel_subviewport_container.gd │ └── smooth_pixel_subviewport_container.gdshader └── example ├── example_scene.tscn ├── gameplay.tscn ├── sprite.png └── sprite.png.import /addons/smooth_pixel_subviewport_container/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Apples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/README.md: -------------------------------------------------------------------------------- 1 | # SmoothPixelSubViewportContainer 2 | 3 | (Main repository: https://github.com/apples/godot-smooth-pixel-subviewport-container) 4 | 5 | Smooth camera and anti-aliasing filter for pixel-perfect games. 6 | 7 | Smooth camera inspired by . 8 | 9 | Anti-aliasing inspired by . 10 | 11 | ## Documentation 12 | 13 | Check documentation comments in the code, or browse the docs in Godot's build-in help viewer. 14 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/SmoothPixelSubViewportContainer.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/SmoothPixelSubViewportContainer.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://cx2oi71a4two5" 6 | path="res://.godot/imported/SmoothPixelSubViewportContainer.svg-a59571622b782a1c7ebc6b0f9d523356.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://addons/smooth_pixel_subviewport_container/SmoothPixelSubViewportContainer.svg" 14 | dest_files=["res://.godot/imported/SmoothPixelSubViewportContainer.svg-a59571622b782a1c7ebc6b0f9d523356.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="SmoothPixelSubViewportContainer" 4 | description="A SubViewportContainer which eliminates camera jitter and applies an anti-aliasing filter to pixel edges." 5 | author="Apples" 6 | version="0.1.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | func _enter_tree() -> void: 5 | add_custom_type( 6 | "SmoothPixelSubViewportContainer", 7 | "SubViewportContainer", 8 | preload("smooth_pixel_subviewport_container.gd"), 9 | preload("SmoothPixelSubViewportContainer.svg")) 10 | 11 | func _exit_tree() -> void: 12 | remove_custom_type("SmoothPixelSubViewportContainer") 13 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/smooth_pixel_subviewport_container.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends SubViewportContainer 3 | 4 | ## A [SubViewportContainer] which eliminates camera jitter and applies an 5 | ## anti-aliasing filter to pixel edges. 6 | ## 7 | ## To use: Add a [SmoothPixelSubViewportContainer] node to your top-level scene, 8 | ## and add a [SubViewport] as a child of that container. [br][br] 9 | ## 10 | ## The SubViewport [b]MUST[/b] have a [member SubViewport.size] that is 11 | ## at least 2 pixels larger than your game's unscaled resolution. 12 | ## For example, if you are targeting a pixel art screen size of [code]400x240[/code], 13 | ## the SubViewport's size must be [code]402x242[/code]. [br][br] 14 | ## 15 | ## The Container must also be shifted slightly offscreen 16 | ## such that the SubViewport's extra 2 pixels form a 1 pixel offscreen border. 17 | ## In most cases, if your [member ProjectSettings.display/window/size/viewport_width] 18 | ## and [member ProjectSettings.display/window/size/viewport_height] match your 19 | ## pixel art screen size, the Container simply needs a position of [code](-1, -1)[/code]. 20 | ## Ensure that the Container covers your screen area. [br][br] 21 | ## 22 | ## The SubViewport should contain your entire gameplay scene. 23 | ## If you want crisp UI textures and fonts, keep your UI outside of the 24 | ## container. [br][br] 25 | ## 26 | ## Most likely, you'll want to change some settings on your SubViewport: [br] 27 | ## - [member SubViewport.snap_2d_transforms_to_pixel]: Should be On. 28 | ## (Or use [member SubViewport.snap_2d_vertices_to_pixel].) [br] 29 | ## - [member SubViewport.canvas_item_default_texture_filter]: Should be Nearest. [br][br] 30 | ## 31 | ## Sample node tree: 32 | ## [codeblock lang=text] 33 | ## (Node) GameRoot 34 | ## | 35 | ## +- (SmoothPixelSubViewportContainer) SubViewportContainer 36 | ## | | position = (-1, -1) 37 | ## | | 38 | ## | +- (SubViewport) SubViewport 39 | ## | | size = (402, 242) 40 | ## | | snap_2d_transforms_to_pixel = true 41 | ## | | canvas_item_default_texture_filter = NEAREST 42 | ## | | 43 | ## | +- (Scene) GameplayScene 44 | ## | 45 | ## +- (Scene) UIScene 46 | ## [/codeblock] 47 | ## 48 | 49 | const SHADER = preload("smooth_pixel_subviewport_container.gdshader") 50 | 51 | ## Smooths camera motion. When disabled, the camera will snap to pixels. 52 | @export var smoothcam_enabled: bool = true: set = enable_smoothcam 53 | 54 | ## Anti-aliases the edges of pixels. 55 | @export var antialiasing_enabled: bool = true: set = enable_antialiasing 56 | 57 | var _smooth_viewport_shader_material: ShaderMaterial 58 | 59 | func _ready() -> void: 60 | _smooth_viewport_shader_material = ShaderMaterial.new() 61 | _smooth_viewport_shader_material.shader = SHADER 62 | _configure() 63 | 64 | func _enter_tree() -> void: 65 | if is_node_ready(): 66 | _configure() 67 | 68 | func _exit_tree() -> void: 69 | if RenderingServer.frame_pre_draw.is_connected(_on_rendering_server_frame_pre_draw): 70 | RenderingServer.frame_pre_draw.disconnect(_on_rendering_server_frame_pre_draw) 71 | 72 | func _get_configuration_warnings() -> PackedStringArray: 73 | var first_subviewport: SubViewport = null 74 | for c in get_children(): 75 | if c is SubViewport: 76 | if first_subviewport != null: 77 | return ["SmoothPixelSubViewportContainer only allows a single SubViewport child."] 78 | first_subviewport = c 79 | return [] 80 | 81 | func enable_smoothcam(v: bool) -> void: 82 | if smoothcam_enabled == v: 83 | return 84 | smoothcam_enabled = v 85 | if is_inside_tree(): 86 | _configure() 87 | 88 | func enable_antialiasing(v: bool) -> void: 89 | if antialiasing_enabled == v: 90 | return 91 | antialiasing_enabled = v 92 | if is_inside_tree(): 93 | _configure() 94 | 95 | func _configure() -> void: 96 | if smoothcam_enabled or antialiasing_enabled: 97 | material = _smooth_viewport_shader_material 98 | else: 99 | material = null 100 | 101 | if smoothcam_enabled: 102 | if not RenderingServer.frame_pre_draw.is_connected(_on_rendering_server_frame_pre_draw): 103 | RenderingServer.frame_pre_draw.connect(_on_rendering_server_frame_pre_draw) 104 | else: 105 | if RenderingServer.frame_pre_draw.is_connected(_on_rendering_server_frame_pre_draw): 106 | RenderingServer.frame_pre_draw.disconnect(_on_rendering_server_frame_pre_draw) 107 | 108 | if antialiasing_enabled: 109 | texture_filter = TEXTURE_FILTER_LINEAR 110 | texture_repeat = TEXTURE_REPEAT_DISABLED 111 | else: 112 | texture_filter = TEXTURE_FILTER_PARENT_NODE 113 | texture_repeat = TEXTURE_REPEAT_PARENT_NODE 114 | 115 | func _on_rendering_server_frame_pre_draw() -> void: 116 | var subviewport: SubViewport = null 117 | for c in get_children(): 118 | if c is SubViewport: 119 | subviewport = c 120 | break 121 | if subviewport == null: 122 | return 123 | 124 | var camera_position := subviewport.canvas_transform.origin 125 | var rounded_position := camera_position.round() 126 | var offset := camera_position - rounded_position 127 | subviewport.canvas_transform.origin = rounded_position 128 | _smooth_viewport_shader_material.set_shader_parameter("vertex_offset", offset) 129 | -------------------------------------------------------------------------------- /addons/smooth_pixel_subviewport_container/smooth_pixel_subviewport_container.gdshader: -------------------------------------------------------------------------------- 1 | // Implements smooth camera motion and anti-aliasing for pixel art games. 2 | 3 | shader_type canvas_item; 4 | 5 | /** Used for camera smoothing. Set automatically by SmoothPixelSubViewportContainer. */ 6 | uniform vec2 vertex_offset; 7 | 8 | void vertex() { 9 | // Camera smoothing. 10 | VERTEX += vertex_offset; 11 | } 12 | 13 | void fragment() { 14 | // Pixel edge anti-aliasing. 15 | 16 | // Thanks to mortarroad for the inspiration and original shader code. 17 | // 18 | 19 | vec2 uv_per_pixel = fwidth(UV); 20 | 21 | vec2 nearest_texel_corner = round(UV / TEXTURE_PIXEL_SIZE); 22 | vec2 nearest_texel_corner_uv = nearest_texel_corner * TEXTURE_PIXEL_SIZE; 23 | 24 | vec2 uv_dist_to_nearest = UV - nearest_texel_corner_uv; 25 | vec2 pixel_dist_to_nearest = uv_dist_to_nearest / uv_per_pixel; 26 | 27 | vec2 bilinear_filter_value = clamp(pixel_dist_to_nearest, -0.5, 0.5); 28 | vec2 sharp_uv = (nearest_texel_corner + bilinear_filter_value) * TEXTURE_PIXEL_SIZE; 29 | 30 | COLOR = texture(TEXTURE, sharp_uv); 31 | } 32 | -------------------------------------------------------------------------------- /example/example_scene.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://qcc2elan73h8"] 2 | 3 | [ext_resource type="Shader" path="res://addons/smooth_pixel_subviewport_container/smooth_pixel_subviewport_container.gdshader" id="1_slk76"] 4 | [ext_resource type="Script" path="res://addons/smooth_pixel_subviewport_container/smooth_pixel_subviewport_container.gd" id="2_wx77v"] 5 | [ext_resource type="PackedScene" uid="uid://ccvueio3bejfb" path="res://example/gameplay.tscn" id="3_3lx35"] 6 | 7 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_kp674"] 8 | shader = ExtResource("1_slk76") 9 | shader_parameter/vertex_offset = Vector2(0, 0) 10 | 11 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_2bso2"] 12 | shader = ExtResource("1_slk76") 13 | shader_parameter/vertex_offset = null 14 | 15 | [node name="ExampleScene" type="Node"] 16 | 17 | [node name="SmoothPixelSubViewportContainer" type="SubViewportContainer" parent="."] 18 | texture_filter = 2 19 | texture_repeat = 1 20 | material = SubResource("ShaderMaterial_kp674") 21 | offset_left = -1.0 22 | offset_top = -1.0 23 | offset_right = 401.0 24 | offset_bottom = 241.0 25 | script = ExtResource("2_wx77v") 26 | 27 | [node name="SubViewport" type="SubViewport" parent="SmoothPixelSubViewportContainer"] 28 | handle_input_locally = false 29 | snap_2d_transforms_to_pixel = true 30 | canvas_item_default_texture_filter = 0 31 | size = Vector2i(402, 242) 32 | render_target_update_mode = 4 33 | 34 | [node name="Gameplay" parent="SmoothPixelSubViewportContainer/SubViewport" instance=ExtResource("3_3lx35")] 35 | 36 | [node name="SmoothPixelSubViewportContainer2" type="SubViewportContainer" parent="."] 37 | texture_filter = 2 38 | texture_repeat = 1 39 | material = SubResource("ShaderMaterial_2bso2") 40 | offset_left = -1.0 41 | offset_top = -1.0 42 | offset_right = 401.0 43 | offset_bottom = 241.0 44 | script = ExtResource("2_wx77v") 45 | smoothcam_enabled = false 46 | 47 | [node name="SubViewport" type="SubViewport" parent="SmoothPixelSubViewportContainer2"] 48 | transparent_bg = true 49 | handle_input_locally = false 50 | snap_2d_transforms_to_pixel = true 51 | canvas_item_default_texture_filter = 0 52 | size = Vector2i(402, 242) 53 | render_target_update_mode = 4 54 | 55 | [node name="CanvasLayer" type="CanvasLayer" parent="SmoothPixelSubViewportContainer2/SubViewport"] 56 | 57 | [node name="ColorRect" type="ColorRect" parent="SmoothPixelSubViewportContainer2/SubViewport/CanvasLayer"] 58 | offset_left = 16.0 59 | offset_top = 24.0 60 | offset_right = 40.0 61 | offset_bottom = 48.0 62 | color = Color(1, 0, 0, 1) 63 | 64 | [node name="ColorRect2" type="ColorRect" parent="SmoothPixelSubViewportContainer2/SubViewport/CanvasLayer"] 65 | offset_left = 85.0 66 | offset_top = 24.0 67 | offset_right = 109.0 68 | offset_bottom = 48.0 69 | color = Color(1, 0, 0, 1) 70 | 71 | [node name="ColorRect3" type="ColorRect" parent="SmoothPixelSubViewportContainer2/SubViewport/CanvasLayer"] 72 | offset_left = 50.0 73 | offset_top = 24.0 74 | offset_right = 74.0 75 | offset_bottom = 48.0 76 | color = Color(1, 0, 0, 1) 77 | -------------------------------------------------------------------------------- /example/gameplay.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://ccvueio3bejfb"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://ble3iml5as5pp" path="res://example/sprite.png" id="1_tai38"] 4 | 5 | [sub_resource type="GDScript" id="GDScript_ckjd3"] 6 | resource_name = "Camera" 7 | script/source = "extends Camera2D 8 | 9 | var t = 0.0 10 | 11 | func _process(delta: float) -> void: 12 | t += delta * 0.05 13 | position = Vector2(sin(t*7.0),cos(t*13.0)) * 50.0 14 | " 15 | 16 | [node name="Gameplay" type="Node2D"] 17 | 18 | [node name="Camera2D" type="Camera2D" parent="."] 19 | ignore_rotation = false 20 | script = SubResource("GDScript_ckjd3") 21 | 22 | [node name="Sprite" type="Sprite2D" parent="."] 23 | texture = ExtResource("1_tai38") 24 | -------------------------------------------------------------------------------- /example/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apples/godot-smooth-pixel-subviewport-container/b49fdf5985d341a582989c462705f3a904b38a8c/example/sprite.png -------------------------------------------------------------------------------- /example/sprite.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://ble3iml5as5pp" 6 | path="res://.godot/imported/sprite.png-b9e388852b51a2944d0b4c2e7aaa0de0.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://example/sprite.png" 14 | dest_files=["res://.godot/imported/sprite.png-b9e388852b51a2944d0b4c2e7aaa0de0.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | --------------------------------------------------------------------------------