└── addons └── crt ├── crt.gd.uid ├── plugin.gd.uid ├── crt.gdshader.uid ├── plugin.gd ├── plugin.cfg ├── icon.svg ├── crt.gd └── crt.gdshader /addons/crt/crt.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bghaddibpt6f1 2 | -------------------------------------------------------------------------------- /addons/crt/plugin.gd.uid: -------------------------------------------------------------------------------- 1 | uid://e1ckld2u6fh5 2 | -------------------------------------------------------------------------------- /addons/crt/crt.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://cv7hfm5duhalc 2 | -------------------------------------------------------------------------------- /addons/crt/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | -------------------------------------------------------------------------------- /addons/crt/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="CRT" 4 | description="A high-quality CRT (Cathode Ray Tube) shader for Godot 4.x that simulates the look and feel of old CRT displays with various customizable effects." 5 | author="nofacer" 6 | version="1.0.1" 7 | script="plugin.gd" 8 | -------------------------------------------------------------------------------- /addons/crt/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /addons/crt/crt.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | @icon("res://addons/crt/icon.svg") 3 | class_name CRT 4 | extends CanvasLayer 5 | 6 | # Material and update control 7 | var material: ShaderMaterial 8 | @export var update_in_editor: bool = true: 9 | set(value): 10 | update_in_editor = value 11 | if _should_update(): 12 | visible = true 13 | else: 14 | visible = false 15 | 16 | # Base resolution for pixel-perfect effects 17 | @export var resolution: Vector2 = Vector2(320.0, 180.0): 18 | set(value): 19 | resolution = value 20 | _set_shader_param("resolution", resolution) 21 | 22 | # Scanline effect 23 | @export_range(0.0, 1.0) var scan_line_amount: float = 1.0: 24 | set(value): 25 | scan_line_amount = value 26 | _set_shader_param("scan_line_amount", value) 27 | 28 | @export_range(-12.0, -1.0) var scan_line_strength: float = -8.0: 29 | set(value): 30 | scan_line_strength = value 31 | _set_shader_param("scan_line_strength", value) 32 | 33 | # Screen curvature/warp effect 34 | @export_range(0.0, 5.0) var warp_amount: float = 0.1: 35 | set(value): 36 | warp_amount = value 37 | _set_shader_param("warp_amount", value) 38 | 39 | # Visual noise/interference 40 | @export_range(0.0, 0.3) var noise_amount: float = 0.03: 41 | set(value): 42 | noise_amount = value 43 | _set_shader_param("noise_amount", value) 44 | 45 | @export_range(0.0, 1.0) var interference_amount: float = 0.2: 46 | set(value): 47 | interference_amount = value 48 | _set_shader_param("interference_amount", value) 49 | 50 | # Shadow mask/grille 51 | @export_range(0.0, 1.0) var grille_amount: float = 0.1: 52 | set(value): 53 | grille_amount = value 54 | _set_shader_param("grille_amount", value) 55 | 56 | @export_range(1.0, 5.0) var grille_size: float = 1.0: 57 | set(value): 58 | grille_size = value 59 | _set_shader_param("grille_size", value) 60 | 61 | # Vignette 62 | @export_range(0.0, 2.0) var vignette_amount: float = 0.6: 63 | set(value): 64 | vignette_amount = value 65 | _set_shader_param("vignette_amount", value) 66 | 67 | @export_range(0.0, 1.0) var vignette_intensity: float = 0.4: 68 | set(value): 69 | vignette_intensity = value 70 | _set_shader_param("vignette_intensity", value) 71 | 72 | # Chromatic aberration 73 | @export_range(0.0, 1.0) var aberation_amount: float = 0.5: 74 | set(value): 75 | aberation_amount = value 76 | _set_shader_param("aberation_amount", value) 77 | 78 | # Rolling line effect 79 | @export_range(0.0, 1.0) var roll_line_amount: float = 0.3: 80 | set(value): 81 | roll_line_amount = value 82 | _set_shader_param("roll_line_amount", value) 83 | 84 | @export_range(-8.0, 8.0) var roll_speed: float = 1.0: 85 | set(value): 86 | roll_speed = value 87 | _set_shader_param("roll_speed", value) 88 | 89 | # Pixel sharpness/softness 90 | @export_range(-4.0, 0.0) var pixel_strength: float = -2.0: 91 | set(value): 92 | pixel_strength = value 93 | _set_shader_param("pixel_strength", value) 94 | 95 | 96 | func _ready() -> void: 97 | var color_rect = ColorRect.new() 98 | color_rect.color = Color.WHITE 99 | color_rect.set_anchors_preset(Control.PRESET_FULL_RECT) 100 | 101 | material = ShaderMaterial.new() 102 | material.shader = load("res://addons/crt/crt.gdshader") 103 | 104 | # Initialize all shader parameters 105 | _set_shader_param("resolution", resolution) 106 | _set_shader_param("scan_line_amount", scan_line_amount) 107 | _set_shader_param("scan_line_strength", scan_line_strength) 108 | _set_shader_param("warp_amount", warp_amount) 109 | _set_shader_param("noise_amount", noise_amount) 110 | _set_shader_param("interference_amount", interference_amount) 111 | _set_shader_param("grille_amount", grille_amount) 112 | _set_shader_param("grille_size", grille_size) 113 | _set_shader_param("vignette_amount", vignette_amount) 114 | _set_shader_param("vignette_intensity", vignette_intensity) 115 | _set_shader_param("aberation_amount", aberation_amount) 116 | _set_shader_param("roll_line_amount", roll_line_amount) 117 | _set_shader_param("roll_speed", roll_speed) 118 | _set_shader_param("pixel_strength", pixel_strength) 119 | 120 | color_rect.material = material 121 | add_child(color_rect) 122 | if _should_update(): 123 | visible = true 124 | else: 125 | visible = false 126 | 127 | 128 | # Helper function to safely set shader parameters 129 | func _set_shader_param(param_name: String, value) -> void: 130 | if material: 131 | material.set_shader_parameter(param_name, value) 132 | 133 | 134 | func _should_update() -> bool: 135 | if Engine.is_editor_hint(): 136 | return update_in_editor 137 | return true 138 | -------------------------------------------------------------------------------- /addons/crt/crt.gdshader: -------------------------------------------------------------------------------- 1 | // CRT Shader for Godot Engine 2 | // Simulates the look of old CRT displays with various customizable effects 3 | shader_type canvas_item; 4 | 5 | // Built-in texture for screen sampling 6 | uniform sampler2D SCREEN_TEXTURE: hint_screen_texture; 7 | 8 | // Base resolution for pixel-perfect effects (lower values = larger pixels) 9 | uniform vec2 resolution = vec2(320.0, 180.0); 10 | 11 | // ===== Effect Controls ===== 12 | // Each parameter has a sensible default and range hint for the Godot editor 13 | 14 | // Scanline effect (dark lines between pixel rows) 15 | uniform float scan_line_amount : hint_range(0.0, 1.0) = 1.0; 16 | uniform float scan_line_strength : hint_range(-12.0, -1.0) = -8.0; 17 | 18 | // Screen curvature/warp effect 19 | uniform float warp_amount : hint_range(0.0, 5.0) = 0.1; 20 | 21 | // Visual noise/interference 22 | uniform float noise_amount : hint_range(0.0, 0.3) = 0.03; 23 | uniform float interference_amount : hint_range(0.0, 1.0) = 0.2; 24 | 25 | // Shadow mask/grille (simulates color phosphor patterns) 26 | uniform float grille_amount : hint_range(0.0, 1.0) = 0.1; 27 | uniform float grille_size : hint_range(1.0, 5.0) = 1.0; 28 | 29 | // Vignette (darkening at screen edges) 30 | uniform float vignette_amount : hint_range(0.0, 2.0) = 0.6; 31 | uniform float vignette_intensity : hint_range(0.0, 1.0) = 0.4; 32 | 33 | // Chromatic aberration (color separation) 34 | uniform float aberation_amount : hint_range(0.0, 1.0) = 0.5; 35 | 36 | // Rolling line effect (simulates VHS/analog interference) 37 | uniform float roll_line_amount : hint_range(0.0, 1.0) = 0.3; 38 | uniform float roll_speed : hint_range(-8.0, 8.0) = 1.0; 39 | 40 | // Pixel sharpness/softness 41 | uniform float pixel_strength : hint_range(-4.0, 0.0) = -2.0; 42 | 43 | // ===== Utility Functions ===== 44 | 45 | /** 46 | * Generates a pseudo-random float between 0 and 1 based on input coordinates 47 | */ 48 | float random(vec2 uv) { 49 | return fract(cos(uv.x * 83.4827 + uv.y * 92.2842) * 43758.5453123); 50 | } 51 | 52 | /** 53 | * Samples a pixel from the screen texture with optional offset and noise 54 | * Handles edge cases and applies noise if enabled 55 | */ 56 | vec3 fetch_pixel(vec2 uv, vec2 off) { 57 | // Calculate pixel position with offset and snap to pixel grid 58 | vec2 pos = floor(uv * resolution + off) / resolution + vec2(0.5) / resolution; 59 | 60 | // Apply noise if enabled 61 | float noise = 0.0; 62 | if (noise_amount > 0.0) { 63 | noise = random(pos + fract(TIME)) * noise_amount; 64 | } 65 | 66 | // Clamp to screen bounds (return black if outside) 67 | if (max(abs(pos.x - 0.5), abs(pos.y - 0.5)) > 0.5) { 68 | return vec3(0.0); 69 | } 70 | 71 | // Sample the texture with mipmap bias for sharper pixels 72 | return texture(SCREEN_TEXTURE, pos, -16.0).rgb + noise; 73 | } 74 | 75 | // ===== Pixel and Scanline Processing ===== 76 | 77 | /** 78 | * Calculates distance from current pixel to nearest texel center 79 | * Used for sub-pixel filtering and anti-aliasing 80 | */ 81 | vec2 Dist(vec2 pos) { 82 | pos = pos * resolution; 83 | return -((pos - floor(pos)) - vec2(0.5)); 84 | } 85 | 86 | /** 87 | * 1D Gaussian function for smooth filtering 88 | */ 89 | float Gaus(float pos, float scale) { 90 | return exp2(scale * pos * pos); 91 | } 92 | 93 | /** 94 | * 3-tap Gaussian filter along horizontal line 95 | * Creates smooth horizontal blending between pixels 96 | */ 97 | vec3 Horz3(vec2 pos, float off) { 98 | // Sample three horizontally adjacent pixels 99 | vec3 b = fetch_pixel(pos, vec2(-1.0, off)); 100 | vec3 c = fetch_pixel(pos, vec2( 0.0, off)); 101 | vec3 d = fetch_pixel(pos, vec2( 1.0, off)); 102 | 103 | // Calculate weights based on distance from pixel center 104 | float dst = Dist(pos).x; 105 | float wb = Gaus(dst - 1.0, pixel_strength); 106 | float wc = Gaus(dst + 0.0, pixel_strength); 107 | float wd = Gaus(dst + 1.0, pixel_strength); 108 | 109 | // Return weighted average of samples 110 | return (b * wb + c * wc + d * wd) / (wb + wc + wd); 111 | } 112 | 113 | /** 114 | * Calculates scanline weight for a given position and offset 115 | * Creates the dark lines between pixel rows 116 | */ 117 | float Scan(vec2 pos, float off) { 118 | float dst = Dist(pos).y; 119 | return Gaus(dst + off, scan_line_strength); 120 | } 121 | 122 | /** 123 | * Applies scanline effect by blending multiple horizontal samples 124 | * Combines three scanlines with gaussian weights for smooth transitions 125 | */ 126 | vec3 Tri(vec2 pos) { 127 | vec3 clr = fetch_pixel(pos, vec2(0.0)); 128 | 129 | if (scan_line_amount > 0.0) { 130 | // Sample three vertically adjacent scanlines 131 | vec3 a = Horz3(pos, -1.0); 132 | vec3 b = Horz3(pos, 0.0); 133 | vec3 c = Horz3(pos, 1.0); 134 | 135 | // Calculate scanline weights 136 | float wa = Scan(pos, -1.0); 137 | float wb = Scan(pos, 0.0); 138 | float wc = Scan(pos, 1.0); 139 | 140 | // Blend between original and scanline-affected color 141 | vec3 scanlines = a * wa + b * wb + c * wc; 142 | clr = mix(clr, scanlines, scan_line_amount); 143 | } 144 | 145 | return clr; 146 | } 147 | 148 | // ===== Screen Warping and Distortion Effects ===== 149 | 150 | /** 151 | * Applies a spherize/bulge distortion to simulate CRT screen curvature 152 | * Warps UV coordinates more severely towards screen edges 153 | */ 154 | vec2 warp(vec2 uv) { 155 | // Calculate distance from center and apply non-linear warping 156 | vec2 delta = uv - 0.5; 157 | float delta2 = dot(delta.xy, delta.xy); 158 | float delta4 = delta2 * delta2; 159 | float delta_offset = delta4 * warp_amount; 160 | 161 | // Apply warping and normalize back to 0-1 range 162 | vec2 warped = uv + delta * delta_offset; 163 | return (warped - 0.5) / mix(1.0, 1.2, warp_amount/5.0) + 0.5; 164 | } 165 | 166 | /** 167 | * Applies vignette effect (darkening at screen edges) 168 | * Uses a smooth falloff based on distance from center 169 | */ 170 | float vignette(vec2 uv) { 171 | // Create circular falloff from center 172 | uv *= 1.0 - uv.xy; 173 | float vignette = uv.x * uv.y * 15.0; 174 | 175 | // Apply intensity controls 176 | return pow(vignette, vignette_intensity * vignette_amount); 177 | } 178 | 179 | /** 180 | * Simulates a shadow mask/grille pattern like those found in CRT displays 181 | * Creates color separation similar to real phosphor patterns 182 | */ 183 | vec3 grille(vec2 uv) { 184 | float unit = PI / 3.0; 185 | float scale = 2.0 * unit / grille_size; 186 | 187 | // Create offset color patterns for RGB channels 188 | float r = smoothstep(0.5, 0.8, cos(uv.x * scale - unit)); 189 | float g = smoothstep(0.5, 0.8, cos(uv.x * scale + unit)); 190 | float b = smoothstep(0.5, 0.8, cos(uv.x * scale + 3.0 * unit)); 191 | 192 | // Blend between original color and grille pattern 193 | return mix(vec3(1.0), vec3(r, g, b), grille_amount); 194 | } 195 | 196 | /** 197 | * Creates rolling horizontal interference lines 198 | * Simulates analog signal interference or VHS artifacts 199 | */ 200 | float roll_line(vec2 uv) { 201 | // Create complex wave pattern that moves with time 202 | float x = uv.y * 3.0 - TIME * roll_speed; 203 | float f = cos(x) * cos(x * 2.35 + 1.1) * cos(x * 4.45 + 2.3); 204 | 205 | // Threshold and smooth the wave to create distinct lines 206 | float roll_line = smoothstep(0.5, 0.9, f); 207 | return roll_line * roll_line_amount; 208 | } 209 | 210 | // ===== Main Shader Function ===== 211 | 212 | void fragment() { 213 | // Get screen coordinates and apply screen warping 214 | vec2 pix = FRAGCOORD.xy; 215 | vec2 pos = warp(SCREEN_UV); 216 | 217 | // Generate rolling interference lines if enabled 218 | float line = 0.0; 219 | if (roll_line_amount > 0.0) { 220 | line = roll_line(pos); 221 | } 222 | 223 | // Apply horizontal interference/jitter 224 | vec2 sq_pix = floor(pos * resolution) / resolution + vec2(0.5) / resolution; 225 | if (interference_amount + roll_line_amount > 0.0) { 226 | float interference = random(sq_pix.yy + fract(TIME)); 227 | pos.x += (interference * (interference_amount + line * 6.0)) / resolution.x; 228 | } 229 | 230 | // Get base color with scanline processing 231 | vec3 clr = Tri(pos); 232 | 233 | // Apply chromatic aberration (color separation) 234 | if (aberation_amount > 0.0) { 235 | float chromatic = aberation_amount + line * 2.0; // Enhanced by rolling lines 236 | vec2 chromatic_x = vec2(chromatic, 0.0) / resolution.x; 237 | vec2 chromatic_y = vec2(0.0, chromatic/2.0) / resolution.y; 238 | 239 | // Sample RGB channels with slight offsets 240 | float r = Tri(pos - chromatic_x).r; // Red shifted left 241 | float g = Tri(pos + chromatic_y).g; // Green shifted down 242 | float b = Tri(pos + chromatic_x).b; // Blue shifted right 243 | clr = vec3(r, g, b); 244 | } 245 | 246 | // Apply shadow mask/grille effect 247 | if (grille_amount > 0.0) { 248 | clr *= grille(pix); 249 | } 250 | 251 | // Adjust overall brightness based on effects 252 | clr *= 1.0 + scan_line_amount * 0.6 + line * 3.0 + grille_amount * 2.0; 253 | 254 | // Apply vignette (darken edges) 255 | if (vignette_amount > 0.0) { 256 | clr *= vignette(pos); 257 | } 258 | 259 | // Output final color 260 | COLOR.rgb = clr; 261 | COLOR.a = 1.0; 262 | } --------------------------------------------------------------------------------