1. 项目概述为什么我们需要一个复古CRT着色器如果你和我一样是个对像素风游戏情有独钟的开发者那你肯定不止一次地怀念过老式显像管CRT显示器带来的那种独特质感。那种微微闪烁的扫描线、屏幕边缘的弧度、色彩边缘的轻微溢出甚至是模拟信号干扰带来的“雪花”噪点这些都不是简单的像素块能带来的。它们共同构成了一种难以言喻的“氛围感”是复古游戏体验的灵魂所在。在Godot 4.x里我们有了强大的现代渲染管线但默认的清晰锐利反而让像素风显得有些“干瘪”。直接拉伸像素块边缘会变得像乐高积木一样生硬而简单的线性过滤又会让画面糊成一团丢失像素风的精髓。这时候一个精心编写的CRT着色器Shader就成了连接现代引擎与复古美学的桥梁。它不是一个简单的滤镜而是一套基于物理和感知的视觉增强方案通过模拟CRT显示器的光学特性让像素画面“活”起来重新获得那种温暖、有机且充满活力的观感。这个方案的核心价值在于它用现代GPU的计算能力去“欺骗”玩家的眼睛重现一种已经消失的硬件体验。这不仅是为了怀旧更是为了提升游戏的整体艺术表现力。一个合适的CRT着色器能让低分辨率的像素艺术在4K屏幕上依然保持魅力甚至掩盖一些因资源限制带来的美术瑕疵。接下来我们就从原理开始一步步在Godot 4.x中实现一个功能完整、效果可调的复古CRT着色器。2. CRT效果核心原理拆解不只是几条扫描线很多人以为CRT效果就是加几条灰色的横线这其实是个巨大的误解。一个逼真的CRT模拟是多种光学和电子现象复合的结果。理解这些原理是我们写出高质量着色器的第一步。2.1 扫描线与光栅这是最标志性的特征。老式CRT显示器通过电子枪从左到右、从上到下逐行轰击屏幕内侧的荧光粉来成像。由于荧光粉的余晖效应和人类视觉暂留我们看到的是一幅完整图像但实际上图像是由一行行独立的“线”组成的。原理在着色器中我们通过取屏幕空间坐标的y分量即UV.y对其应用一个周期性的函数如sin或fract来创建明暗交替的条纹。关键点在于这些条纹的亮度不是简单的黑白而是与原始像素颜色进行混合。通常扫描线覆盖的区域会变暗模拟电子束未扫描到的区域而扫描线之间的区域则保持原亮度或稍亮。实现细节扫描线的粗细频率、对比度强度和锐利度是需要精细调校的参数。太粗会破坏画面太细则看不见对比度太高会让画面割裂太低则没效果。一个技巧是让扫描线的暗度与像素本身的亮度挂钩亮部区域的扫描线可以更不明显这样能更好地融入画面。2.2 屏幕曲率与色差老式CRT的玻璃屏幕并非完全平面尤其是边缘通常带有明显的弧度。这会导致两个视觉效果几何形变画面四角向内收缩边缘的直线会变弯。色散色差由于玻璃对不同波长光线的折射率不同红、绿、蓝三色电子束在屏幕边缘的汇聚点会发生偏移导致高对比度边缘如白色文字在黑色背景上出现红/蓝颜色的镶边。原理在着色器中我们首先对UV坐标进行扭曲。通常使用一个基于到屏幕中心距离的二次或三次函数将边缘的UV向中心挤压。然后对这个扭曲后的UV分别用微小的偏移量去采样红、绿、蓝三个颜色通道再将它们组合起来就能模拟出色差效果。注意事项曲率效果要适度过强的扭曲会让游戏UI和文字难以阅读。通常只为游戏画面区域应用此效果而HUD血条、分数保持平整。色差的强度也应可控它更像一种风格化点缀而非必须的物理精确模拟。2.3 像素网格与色彩混合CRT显示器没有现代LCD那样清晰的“像素点”。它的“像素”是由红、绿、蓝三条荧光粉条组成的电子束的尺寸和聚焦情况会导致相邻的荧光粉条被部分激发从而产生色彩混合和柔化效果。原理这是CRT着色器的精髓也是区别于简单“马赛克”的关键。我们不再把每个逻辑像素当作一个孤立的方块。相反我们将屏幕视为一个由这些微小的“荧光粉三色组”构成的精细网格。着色器需要模拟电子束的强度分布通常近似为高斯分布当它击中屏幕时不仅会点亮目标“像素”的荧光粉还会轻微点亮其相邻的荧光粉。实现思路一种常见且高效的方法是使用“孔径网格”模拟。我们为每个逻辑像素分配一个小的纹理比如3x3或5x5这个纹理定义了电子束在该像素内的强度分布。然后在着色器中对输入的低分辨率纹理进行上采样时不是做简单的最近邻或双线性过滤而是根据这个孔径网格对周围多个像素进行加权采样混合。这样一个“红色”像素的边缘会微微渗入旁边的“绿色”像素产生一种柔和、自然的过渡完美消除了像素风的生硬锯齿。2.4 辉光、模糊与噪点辉光高亮区域如爆炸、灯光的荧光粉被过度激发光晕会扩散到周围区域。这可以通过提取画面高亮部分进行高斯模糊再叠加回原图来实现。模糊除了像素混合带来的固有柔化CRT显示器本身也有轻微的聚焦不准和信号干扰带来的整体模糊感。一个轻微的全屏模糊通道有时能增加“模拟信号”的质感。噪点/雪花模拟信号弱或频道不佳时的随机噪声。在着色器中用噪声函数如rand生成随机灰度值以很低的不透明度叠加到画面上可以增加动态感和复古真实感。实操心得不要试图一次性实现所有效果。建议按照“像素网格混合 - 扫描线 - 曲率 - 色差 - 辉光/噪点”的顺序逐个添加和调试。每个效果都应配有强度(strength)、尺寸(size)等参数并暴露给Godot的材质参数以便在编辑器中实时调节。记住我们的目标是艺术性的增强而非物理精确的模拟因此“看起来对”比“理论上对”更重要。3. Godot 4.x 着色器实战构建完整的CRT材质理解了原理我们开始在Godot 4.x中动手实现。Godot 4的着色器语言基于改进的GLSL ES 3.0并提供了非常直观的ShaderMaterial工作流。3.1 项目设置与基础框架首先创建一个新场景或使用你的主游戏场景。我们需要一个覆盖全屏的矩形来应用后期处理效果。创建全屏ColorRect添加一个ColorRect节点将其锚点设置为“全矩形”使其铺满整个屏幕。将其Material属性新建为一个ShaderMaterial。创建着色器脚本在ShaderMaterial的Shader属性中点击新建创建一个新的着色器。Godot会自动打开着色器编辑器。我们将编写一个片段着色器fragment()函数因为它决定了屏幕上每个像素的最终颜色。首先建立基础框架获取输入纹理和UV坐标。shader_type canvas_item; // 用于2D/CanvasItem的着色器类型 uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; // 自动获取当前屏幕纹理 uniform vec2 texture_size vec2(320.0, 180.0); // 你的游戏原始逻辑分辨率 void fragment() { // 获取当前片段的标准化UV坐标 (0.0 到 1.0) vec2 uv FRAGCOORD.xy / VIEWPORT_SIZE.xy; // 后续所有效果将基于 uv 和 SCREEN_TEXTURE 进行计算 COLOR texture(SCREEN_TEXTURE, uv); }这个框架很简单只是原样输出屏幕内容。texture_size是我们需要重点关注的参数它定义了你的游戏“原生”像素分辨率。所有基于像素网格的计算都将围绕这个分辨率进行。3.2 实现像素网格与色彩混合这是最核心的一步。我们将采用“虚拟像素网格”的方法。uniform float pixel_blend 1.0; // 像素混合强度 uniform float pixel_grid_size 3.0; // 模拟的荧光粉网格精细度 void fragment() { vec2 uv FRAGCOORD.xy / VIEWPORT_SIZE.xy; // 1. 将屏幕UV转换到逻辑像素空间 vec2 pixel_uv uv * texture_size; vec2 pixel_center floor(pixel_uv) 0.5; // 找到当前片段所属逻辑像素的中心 vec2 sub_pixel_coord pixel_uv - pixel_center; // 在单个逻辑像素内的偏移-0.5 到 0.5 // 2. 模拟电子束的高斯分布孔径 float beam_width 0.5 (1.0 - pixel_blend) * 0.5; // 光束宽度受混合强度影响 float beam_profile exp(-(sub_pixel_coord.x * sub_pixel_coord.x sub_pixel_coord.y * sub_pixel_coord.y) / (beam_width * beam_width)); // 3. 为RGB通道施加微小的偏移模拟荧光粉排列可选的进阶效果 vec2 offset_r vec2(0.0); vec2 offset_g vec2(0.0); vec2 offset_b vec2(0.0); // 这里可以添加基于 pixel_grid_size 的微小偏移计算 // 4. 采样并混合 vec3 col; col.r texture(SCREEN_TEXTURE, (pixel_center offset_r) / texture_size).r; col.g texture(SCREEN_TEXTURE, (pixel_center offset_g) / texture_size).g; col.b texture(SCREEN_TEXTURE, (pixel_center offset_b) / texture_size).b; // 5. 应用光束强度分布 col * beam_profile; // 6. 添加网格感可选在像素边界绘制极细的暗线 vec2 grid abs(fract(pixel_uv) - 0.5) * 2.0; float grid_line max(grid.x, grid.y); grid_line pow(grid_line, 8.0); // 让网格线更锐利 col * 1.0 - grid_line * 0.1; // 轻微变暗 COLOR.rgb col; }这段代码做了几件事它将连续屏幕坐标离散化到逻辑像素网格用一个高斯函数模拟电子束使像素中心最亮边缘渐暗最后还添加了非常细微的像素网格线。pixel_blend参数是关键设为0时接近清晰的像素设为1时混合效果最强。3.3 添加扫描线效果扫描线效果需要在像素混合后的颜色上叠加。uniform float scanline_intensity 0.3; // 扫描线强度 uniform float scanline_thickness 1.0; // 扫描线相对粗细 uniform float scanline_bright_boost 1.1; // 扫描线之间区域的亮度提升 void fragment() { // ... [承接上面的像素混合计算得到 col] ... // 扫描线计算 float scanline_y uv.y * texture_size.y * scanline_thickness; // 使用正弦波或锯齿波创建周期性明暗。这里用abs(sin)让线更柔和。 float scanline abs(sin(scanline_y * 3.14159)); // 将波形锐化让暗部更集中 scanline pow(scanline, 4.0); // 混合暗处乘以一个系数亮处稍微提亮 float scanline_factor mix(scanline_bright_boost, 1.0 - scanline_intensity, scanline); col * scanline_factor; COLOR.rgb col; }这里用sin函数生成波浪形的明暗条纹再用pow函数使其变得更“方”更像真实的扫描线。scanline_bright_boost是一个小技巧略微提亮扫描线之间的区域可以增加画面的对比度和通透感。3.4 集成屏幕曲率与色差曲率效果会扭曲UV坐标因此它应该在所有基于原始UV的采样之前应用。色差则在最终输出前对扭曲后的UV进行偏移采样。uniform float curvature 0.05; // 屏幕曲率强度 uniform float chroma_abberation 0.002; // 色差强度 void fragment() { vec2 uv FRAGCOORD.xy / VIEWPORT_SIZE.xy; // --- 屏幕曲率 --- // 将UV从(0,1)映射到(-1,1) vec2 curved_uv uv * 2.0 - 1.0; // 计算到中心的距离 float dist length(curved_uv); // 应用桶形畸变公式 float distortion 1.0 - curvature * dist * dist; // 防止过度扭曲导致UV越界进行钳制 curved_uv * max(distortion, 0.1); // 映射回(0,1) curved_uv curved_uv * 0.5 0.5; // 确保扭曲后的UV仍在有效范围内避免边缘采样错误 if (curved_uv.x 0.0 || curved_uv.x 1.0 || curved_uv.y 0.0 || curved_uv.y 1.0) { COLOR vec4(0.0, 0.0, 0.0, 1.0); // 超出的部分显示为黑边 return; } // --- 色差 --- vec3 chroma_col; chroma_col.r texture(SCREEN_TEXTURE, vec2(curved_uv.x chroma_abberation, curved_uv.y)).r; chroma_col.g texture(SCREEN_TEXTURE, curved_uv).g; chroma_col.b texture(SCREEN_TEXTURE, vec2(curved_uv.x - chroma_abberation, curved_uv.y)).b; // 将 chroma_col 作为后续像素混合、扫描线等效果的输入颜色 vec3 col chroma_col; // ... [接着进行像素混合、扫描线计算] ... COLOR.rgb col; }注意事项曲率计算是一个消耗相对较大的操作尤其是length和除法。在低端设备上如果性能吃紧可以考虑降低曲率精度或者将曲率效果作为一个可选项关闭。色差采样了三次纹理也有一定开销但通常可以接受。3.5 调优与参数整合现在我们将所有效果串联起来并暴露关键参数到Godot的材质面板方便实时调节。最终的shader代码结构大致如下已做简化整合shader_type canvas_item; uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap; // 基础参数 uniform vec2 texture_size vec2(320.0, 180.0); // 像素混合参数 uniform float pixel_blend 0.8; uniform float pixel_grid_strength 0.05; // 扫描线参数 uniform float scanline_intensity 0.25; uniform float scanline_thickness 1.2; uniform float scanline_bright 1.05; // 曲率与色差参数 uniform float curvature 0.03; uniform float chroma_abberation 0.0015; // 辉光与噪点参数 uniform float glow_threshold 0.7; uniform float glow_strength 0.3; uniform float noise_strength 0.01; // 噪声函数 float rand(vec2 co) { return fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453); } void fragment() { vec2 uv FRAGCOORD.xy / VIEWPORT_SIZE.xy; // 1. 曲率扭曲 vec2 curved_uv uv * 2.0 - 1.0; float dist length(curved_uv); float distortion 1.0 - curvature * dist * dist; curved_uv * max(distortion, 0.1); curved_uv curved_uv * 0.5 0.5; if (any(lessThan(curved_uv, vec2(0.0))) || any(greaterThan(curved_uv, vec2(1.0)))) { COLOR vec4(0.0, 0.0, 0.0, 1.0); return; } // 2. 色差采样 vec3 chroma_col; chroma_col.r texture(SCREEN_TEXTURE, vec2(curved_uv.x chroma_abberation, curved_uv.y)).r; chroma_col.g texture(SCREEN_TEXTURE, curved_uv).g; chroma_col.b texture(SCREEN_TEXTURE, vec2(curved_uv.x - chroma_abberation, curved_uv.y)).b; // 3. 像素网格混合 (基于原始逻辑分辨率在曲率前计算逻辑位置更准确这里为简化在色差后处理) vec2 pixel_uv curved_uv * texture_size; vec2 pixel_center floor(pixel_uv) 0.5; vec2 sub_pixel pixel_uv - pixel_center; float beam_width 0.5 (1.0 - pixel_blend) * 0.5; float beam_profile exp(-dot(sub_pixel, sub_pixel) / (beam_width * beam_width)); vec3 col chroma_col * beam_profile; // 4. 像素网格线 vec2 grid abs(fract(pixel_uv) - 0.5) * 2.0; float grid_line pow(max(grid.x, grid.y), 8.0); col * 1.0 - grid_line * pixel_grid_strength; // 5. 扫描线 float scanline abs(sin(curved_uv.y * texture_size.y * 3.14159 * scanline_thickness)); scanline pow(scanline, 4.0); float scanline_factor mix(scanline_bright, 1.0 - scanline_intensity, scanline); col * scanline_factor; // 6. 简单辉光 (基于亮度阈值) float brightness dot(col, vec3(0.2126, 0.7152, 0.0722)); if (brightness glow_threshold) { col mix(col, vec3(1.0), glow_strength * (brightness - glow_threshold)); } // 7. 添加噪点 col (rand(FRAGCOORD.xy TIME) - 0.5) * noise_strength; COLOR.rgb col; }将这段代码复制到你的着色器中然后在Godot编辑器里调整ShaderMaterial的这些uniform参数你就能实时看到各种CRT效果的变化。一个重要的调试技巧是准备一张具有高对比度边缘、纯色块和渐变区域的测试图它能帮你快速判断像素混合、扫描线和色差的效果是否理想。4. 性能优化与常见问题排查一个全功能的CRT着色器计算量不小。在移动设备或低端PC上我们需要考虑优化。4.1 性能优化策略降低采样精度SCREEN_TEXTURE的filter_linear_mipmap提示对性能有帮助但确保你的视口分辨率是合理的。如果游戏本身是320x180输出到1080p屏幕这个缩放由引擎处理着色器内基于texture_size的计算仍然是轻量的。效果分级将效果分为“高”、“中”、“低”三档。低配仅保留像素混合和扫描线。这是核心体验计算量最小。中配在低配基础上加入色差和轻微噪点。去掉计算量较大的曲率和辉光。高配开启所有效果。 你可以通过uniform bool开关来控制不同效果块的执行。预计算与简化sin、pow、length都是相对昂贵的运算。可以考虑使用纹理查找表LUT来替代一些复杂的实时计算但Godot中需要权衡纹理读取的消耗。对于曲率计算可以尝试用dot(curved_uv, curved_uv)代替length(curved_uv)求平方距离避免开方。使用render_mode如果效果是纯2D的确保使用canvas_item渲染模式。如果用于3D视图的后处理可能需要spatial模式并考虑使用Viewport纹理。4.2 常见问题与解决方案实录以下是我在多次实现CRT着色器时踩过的坑和解决方法问题现象可能原因排查与解决思路画面闪烁或撕裂着色器计算不稳定TIME变量使用不当或与垂直同步冲突。检查噪声函数是否过于依赖TIME且变化过快。尝试将TIME乘以一个小的系数如TIME * 0.1来减慢变化。确保在项目设置中开启了垂直同步。屏幕边缘出现奇怪的颜色或条纹曲率计算导致UV坐标超出[0,1]范围采样到了纹理边界之外。就像我们代码中做的必须对扭曲后的UV进行边界检查any(lessThan(...))一旦越界直接返回黑色或边框色避免错误采样。扫描线看起来太“数字感”不自然扫描线公式过于简单只用了一个sin函数。尝试组合多个频率不同的正弦波或者使用锯齿波(fract)加平滑函数。也可以让扫描线的强度与像素亮度关联亮部扫描线减弱。pow(scanline, 4.0)中的指数4.0就是一个重要的调节参数值越大扫描线越细、越锐利。像素混合导致画面整体太模糊pixel_blend参数值太高或beam_width计算过宽。将pixel_blend调低如0.5-0.7。检查beam_width计算公式确保当pixel_blend0时beam_width足够小使得beam_profile接近1即无混合。在高分辨率下效果不明显或错误texture_sizeuniform 未正确设置。着色器仍然基于错误的低分辨率进行计算。确保在GDScript中根据你游戏的实际逻辑分辨率如窗口大小或Viewport大小动态设置着色器的texture_size参数。例如$ColorRect.material.set_shader_parameter(texture_size, Vector2(320, 180))。色差效果方向不对或太强chroma_abberation偏移量应用在了错误的坐标分量上或数值过大。色差通常发生在高对比度边缘的法线方向。我们简单的水平偏移只是一种近似。更物理的模拟需要计算亮度梯度方向。对于风格化水平偏移已足够。将强度值如0.002调小一个数量级试试如0.0002。在Godot 4中SCREEN_TEXTURE为黑色ColorRect所在的CanvasLayer或渲染顺序有问题未能正确捕获屏幕内容。确保使用hint_screen_texture。将包含ColorRect的CanvasLayer的图层Layer设为最高或者确保它在场景树中位于所有需要被处理的节点之后。有时需要延迟一帧应用可以使用texture(SCREEN_TEXTURE, uv, 0.0)中的Lod偏置参数或考虑使用BackBufferCopy节点。最后一点个人体会CRT着色器的参数没有“标准答案”。不同的游戏风格科幻、奇幻、恐怖需要不同的CRT“味道”。恐怖游戏可能需要更强的噪点和更粗的扫描线来营造不安感色彩明快的平台跳跃游戏可能需要更弱的曲率和更精细的像素混合来保持画面清爽。最好的方法是准备几个预设如“街机厅”、“老电视”、“掌机”让玩家可以选择或者根据游戏的不同关卡动态切换这能极大地增强游戏的沉浸感和艺术表现力。把这个着色器当作一个动态的艺术工具而不仅仅是一个静态的滤镜它的潜力会大得多。