Godot引擎自定义屏幕空间后处理特效:从原理到实战
1. 项目概述为什么我们需要屏幕空间特效如果你在Godot里做过3D项目肯定对“后处理”这个词不陌生。简单说它就是游戏画面渲染完成后再对整个屏幕图像进行二次加工的过程。Godot引擎内置了不少开箱即用的后处理效果比如让光源边缘发光的泛光Bloom模拟相机聚焦的景深DOF还有增强物体间阴影细节的屏幕空间环境光遮蔽SSAO。这些效果极大地提升了画面的视觉表现力让游戏从“能玩”变成了“好看”。但内置效果总有不够用的时候。比如你想做一个复古的CRT显示器扫描线效果或者实现一个全屏的像素化风格又或者做一个只在特定区域生效的腐蚀、扭曲特效。这时候你就需要自己动手实现自定义的屏幕空间特效。屏幕空间顾名思义就是特效的计算完全基于最终呈现在屏幕上的二维图像不关心场景中物体的三维位置和关系。这种方法的优势是性能开销相对可控实现灵活是增强游戏艺术风格和沉浸感的利器。这篇文章我就结合自己多年在Godot里折腾着色器的经验带你从零开始彻底搞懂如何实现自定义的屏幕空间后处理特效。无论你是想实现一个简单的色彩滤镜还是复杂的边缘检测、模糊混合甚至是基于深度图的动态效果这里都有你需要的思路和可以直接“抄作业”的代码。2. 核心原理屏幕空间特效是如何工作的要理解自定义后处理首先得明白Godot的渲染管线。简单来说Godot渲染一帧画面的过程可以粗略分为几个阶段场景几何体渲染、光照计算、透明度混合等最终输出一张二维的“屏幕纹理”。后处理就是在这张“屏幕纹理”生成之后但显示到玩家屏幕之前插入的一个或多个处理步骤。2.1 屏幕纹理Screen Texture与着色器Godot通过一个特殊的Uniform变量sampler2D screen_texture并配合hint_screen_texture提示将当前帧的屏幕图像暴露给着色器。这个纹理的UV坐标范围是(0,0)到(1,1)对应整个屏幕。在着色器中你可以像读取普通纹理一样读取screen_texture上任意位置的像素颜色。注意hint_screen_texture这个提示符至关重要。它告诉Godot的着色器编译器这个采样器需要绑定到当前视口的渲染结果上。没有它你的着色器将无法获取到屏幕内容。2.2 后处理的实现载体CanvasLayer与ColorRect在Godot中实现全屏后处理最直接的方法是利用2D渲染系统。具体操作是创建一个CanvasLayer节点。它的作用是将其子节点渲染到一个独立的画布上这个画布默认会覆盖整个屏幕并且层级可以设置确保后处理效果在所有3D/2D场景内容之上。在CanvasLayer下添加一个ColorRect节点。将这个ColorRect节点的锚点Anchors预设设置为“全矩形”Full Rect使其铺满整个屏幕。为ColorRect创建一个ShaderMaterial并在其中编写我们的屏幕空间着色器。这个ColorRect就相当于一个覆盖全屏的“画布”我们的着色器会在这个画布的每一个像素上执行读取screen_texture中对应位置的颜色经过计算后输出新的颜色从而改变最终显示的画面。2.3 单阶段 vs 多阶段处理单阶段处理所有特效计算在一个着色器通道内完成。适合简单的色彩调整、阈值化、简单卷积核如边缘检测等。多阶段处理将复杂的特效拆分成多个连续的着色器通道每个通道的输出作为下一个通道的输入。这对于高斯模糊、景深等需要大量采样或迭代计算的效果至关重要可以大幅降低单个着色器的指令复杂度和纹理采样次数提升性能。例如一个高质量的高斯模糊通常需要在一个方向如水平做一次模糊再将结果传递给下一个着色器在垂直方向做第二次模糊。这种分离的模糊方式采样次数从 NxN 次降到了 NN 次。3. 实战入门构建你的第一个单阶段后处理着色器理论说再多不如动手试一下。我们来创建一个最简单的单阶段特效一个黑白负片颜色反转效果。3.1 场景与节点设置打开Godot创建一个新场景。添加一个CanvasLayer节点命名为PostProcessLayer。在PostProcessLayer下添加一个ColorRect节点命名为InvertEffect。选中InvertEffect在检查器面板中找到Layout菜单点击并选择Full Rect。确保它的尺寸和位置覆盖了整个视口。在InvertEffect的Material属性中新建一个ShaderMaterial。点击新建的ShaderMaterial在其Shader属性中新建一个Shader。3.2 编写负片着色器双击新建的Shader资源打开着色器编辑器。将类型设置为canvas_item因为我们的载体是2D的ColorRect。shader_type canvas_item; // 声明屏幕纹理采样器hint_screen_texture是关键 uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest; void fragment() { // 1. 采样当前屏幕像素的颜色 vec4 screen_color texture(screen_texture, SCREEN_UV); // 2. 计算负片颜色用1.0减去RGB分量Alpha通道保持不变 vec4 inverted_color vec4(vec3(1.0) - screen_color.rgb, screen_color.a); // 3. 输出结果 COLOR inverted_color; }保存着色器。现在运行你的主场景确保主场景的相机能看到一些东西你应该能看到整个画面变成了类似照片底片的效果。SCREEN_UV是Godot内置的变量表示当前片段像素在屏幕纹理上的标准化坐标。实操心得filter_nearest在这里是合适的因为我们通常希望后处理着色器对屏幕纹理进行精确的、不插值的采样。如果你需要做平滑的模糊或缩放可以考虑使用filter_linear。repeat_disable则确保UV坐标超出[0,1]范围时不会重复纹理这对于屏幕空间处理是标准做法。3.3 进阶示例Sobel边缘检测负片太简单了我们来点更酷的边缘检测。Sobel算子是一种常用的图像边缘检测算法。它通过计算像素在水平和垂直方向上的亮度梯度来识别边缘。将InvertEffect的着色器替换为以下代码shader_type canvas_item; uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest; // 边缘检测的敏感度值越小边缘线越细 uniform float edge_threshold : hint_range(0, 1) 0.1; void fragment() { // 获取当前像素和周围8个像素的亮度值 // 为了简化我们使用灰度值 (0.299*R 0.587*G 0.114*B) float i00 dot(texture(screen_texture, SCREEN_UV).rgb, vec3(0.299, 0.587, 0.114)); float i10 dot(texture(screen_texture, SCREEN_UV vec2(SCREEN_PIXEL_SIZE.x, 0.0)).rgb, vec3(0.299, 0.587, 0.114)); float i20 dot(texture(screen_texture, SCREEN_UV vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).rgb, vec3(0.299, 0.587, 0.114)); float i01 dot(texture(screen_texture, SCREEN_UV vec2(0.0, SCREEN_PIXEL_SIZE.y)).rgb, vec3(0.299, 0.587, 0.114)); float i21 dot(texture(screen_texture, SCREEN_UV vec2(0.0, -SCREEN_PIXEL_SIZE.y)).rgb, vec3(0.299, 0.587, 0.114)); float i02 dot(texture(screen_texture, SCREEN_UV vec2(SCREEN_PIXEL_SIZE.x, SCREEN_PIXEL_SIZE.y)).rgb, vec3(0.299, 0.587, 0.114)); float i12 dot(texture(screen_texture, SCREEN_UV vec2(-SCREEN_PIXEL_SIZE.x, SCREEN_PIXEL_SIZE.y)).rgb, vec3(0.299, 0.587, 0.114)); float i22 dot(texture(screen_texture, SCREEN_UV vec2(SCREEN_PIXEL_SIZE.x, -SCREEN_PIXEL_SIZE.y)).rgb, vec3(0.299, 0.587, 0.114)); float i32 dot(texture(screen_texture, SCREEN_UV vec2(-SCREEN_PIXEL_SIZE.x, -SCREEN_PIXEL_SIZE.y)).rgb, vec3(0.299, 0.587, 0.114)); // Sobel算子卷积核 // 水平梯度 Gx float gx (i02 2.0 * i01 i00) - (i22 2.0 * i21 i20); // 垂直梯度 Gy float gy (i00 2.0 * i10 i20) - (i02 2.0 * i12 i22); // 计算梯度幅值 float edge sqrt(gx * gx gy * gy); // 根据阈值判断是否为边缘 if (edge edge_threshold) { COLOR vec4(1.0, 1.0, 1.0, 1.0); // 边缘显示为白色 } else { // 非边缘区域可以显示原图或黑色 COLOR texture(screen_texture, SCREEN_UV); // 或者显示为黑色: COLOR vec4(0.0, 0.0, 0.0, 1.0); } }这个着色器会勾勒出场景中物体的轮廓。你可以通过调整edge_threshold这个Uniform变量来控制检测的灵敏度。在Godot编辑器中选中InvertEffect的ShaderMaterial你会在下方的Shader参数列表中看到edge_threshold可以直接拖动滑块调整。4. 性能进阶实现高效的多阶段高斯模糊单阶段着色器适合轻量级操作。但对于像高斯模糊这种需要大量周边像素采样的操作如果在一个着色器里完成NxN次采样性能开销会很大。标准的优化方法是使用“分离高斯模糊”即分成水平和垂直两个一维模糊通道。4.1 场景结构设计我们需要两个连续的模糊阶段。在之前的PostProcessLayer下再创建两个ColorRect节点分别命名为BlurHorizontal和BlurVertical。确保它们的层级顺序是BlurHorizontal在BlurVertical之上或在场景树中排在更前面。因为Godot的CanvasLayer会按照子节点顺序渲染我们需要先进行水平模糊再将结果传递给垂直模糊。将InvertEffect节点暂时禁用或删除避免干扰。为BlurHorizontal和BlurVertical分别创建ShaderMaterial和Shader。4.2 水平模糊着色器 (BlurHorizontal)这个着色器只对X轴水平方向进行模糊。shader_type canvas_item; uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest; // 模糊半径以像素为单位可以通过Uniform调整 uniform float blur_radius : hint_range(0, 20) 5.0; void fragment() { vec2 pixel_size SCREEN_PIXEL_SIZE; // 单个像素在UV空间的大小 vec4 col vec4(0.0); float total_weight 0.0; // 一维高斯核权重 (近似值半径5) // 实际项目中你可以预计算一个权重数组或者使用更精确的高斯函数。 float weights[11]; // 半径5左右各5个像素中心11 weights[0] 0.0093; weights[1] 0.028; weights[2] 0.066; weights[3] 0.123; weights[4] 0.180; weights[5] 0.208; // 中心权重最大 weights[6] 0.180; weights[7] 0.123; weights[8] 0.066; weights[9] 0.028; weights[10] 0.0093; int kernel_radius 5; // 对应上面数组 for (int i -kernel_radius; i kernel_radius; i) { float weight weights[i kernel_radius]; vec2 offset vec2(float(i) * blur_radius * pixel_size.x, 0.0); col texture(screen_texture, SCREEN_UV offset) * weight; total_weight weight; } // 归一化确保权重和为1防止颜色变亮或变暗 COLOR col / total_weight; }4.3 垂直模糊着色器 (BlurVertical)这个着色器只对Y轴垂直方向进行模糊其输入是BlurHorizontal的输出。shader_type canvas_item; uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest; uniform float blur_radius : hint_range(0, 20) 5.0; void fragment() { vec2 pixel_size SCREEN_PIXEL_SIZE; vec4 col vec4(0.0); float total_weight 0.0; float weights[11]; weights[0] 0.0093; weights[1] 0.028; weights[2] 0.066; weights[3] 0.123; weights[4] 0.180; weights[5] 0.208; weights[6] 0.180; weights[7] 0.123; weights[8] 0.066; weights[9] 0.028; weights[10] 0.0093; int kernel_radius 5; for (int i -kernel_radius; i kernel_radius; i) { float weight weights[i kernel_radius]; vec2 offset vec2(0.0, float(i) * blur_radius * pixel_size.y); col texture(screen_texture, SCREEN_UV offset) * weight; total_weight weight; } COLOR col / total_weight; }现在运行场景你会看到一个柔和的全屏模糊效果。通过调整两个着色器Material中的blur_radius参数可以控制模糊的强度。注意事项这里为了清晰展示了完整的权重数组。在实际高性能要求的项目中我们通常不会在片段着色器中循环这么多次。更优的做法是使用更小的核比如5x5的分离高斯模糊每个方向只需采样5次上面例子是11次半径5。利用双线性采样通过精心选择采样点一次纹理采样可以混合两个像素从而用更少的采样次数模拟更大的核。这被称为“优化高斯模糊”。预计算权重将权重数组作为Uniform数组传入避免在着色器中硬编码。4.4 性能对比单通道 vs 双通道假设我们要实现一个11x11的高斯模糊。单通道实现每个像素需要采样 11 * 11 121 次纹理。分离双通道实现每个像素需要采样 11 11 22 次纹理。性能提升接近5.5倍。对于移动平台或性能敏感的项目这种优化是必须的。5. 高级技巧与深度信息应用基础的屏幕纹理处理已经能实现很多效果但有些高级特效需要更多的信息比如深度Depth、法线Normal、粗糙度Roughness等。Godot 4.x 通过DEPTH_TEXTURE和NORMAL_ROUGHNESS_TEXTURE等内置Uniform提供了对这些缓冲区的访问。5.1 访问深度纹理实现雾效深度纹理存储了每个像素到相机的距离经过投影变换。我们可以用它来实现基于距离的雾效。创建一个新的ColorRect节点和着色器命名为DepthFog。shader_type canvas_item; uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest; // 深度纹理 uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest; // 雾颜色 uniform vec4 fog_color : source_color vec4(0.7, 0.8, 0.9, 1.0); // 雾开始和结束的距离在视图空间通常0-1范围1是远平面 uniform float fog_start : hint_range(0.0, 1.0) 0.2; uniform float fog_end : hint_range(0.0, 1.0) 0.8; void fragment() { vec4 screen_col texture(screen_texture, SCREEN_UV); // 从深度纹理读取深度值。深度纹理的值是非线性的通常在0-1之间。 float depth texture(depth_texture, SCREEN_UV).r; // 将非线性深度转换为线性深度近似。这一步对于正确的距离感很重要。 // 这里使用一个简单的近似公式更精确的转换需要相机的投影矩阵参数。 float linear_depth depth; // 简单起见这里直接用非线性深度。实际项目需要转换。 // 计算雾的浓度因子 (0: 无雾, 1: 完全被雾覆盖) float fog_factor clamp((linear_depth - fog_start) / (fog_end - fog_start), 0.0, 1.0); // 混合原色和雾色 vec3 final_color mix(screen_col.rgb, fog_color.rgb, fog_factor); COLOR vec4(final_color, screen_col.a); }关键点hint_depth_texture提示符告诉Godot将这个采样器绑定到深度缓冲区。深度值的范围和非线性特性取决于你的渲染设置和相机。要获得精确的视图空间线性深度你需要将深度值通过相机的投影矩阵进行逆变换。这通常需要将投影矩阵的逆矩阵作为Uniform传入着色器。这是一个进阶话题但对于实现准确的深度效果如精确的景深、体积光是必要的。5.2 结合法线纹理实现边缘高光Rim Light法线-粗糙度纹理NORMAL_ROUGHNESS_TEXTURE包含了屏幕空间法线信息和粗糙度。我们可以用它来增强物体边缘的视觉表现。shader_type canvas_item; uniform sampler2D screen_texture : hint_screen_texture, repeat_disable, filter_nearest; uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest; uniform sampler2D normal_roughness_texture : hint_normal_roughness_texture, repeat_disable, filter_nearest; uniform vec3 rim_color : source_color vec3(1.0, 0.9, 0.6); uniform float rim_power : hint_range(0.5, 8.0) 3.0; uniform float rim_intensity : hint_range(0.0, 2.0) 0.8; void fragment() { vec4 screen_col texture(screen_texture, SCREEN_UV); // 从法线纹理中解码法线。Godot的法线纹理通常将法线存储在RGB通道粗糙度在A通道。 vec3 normal texture(normal_roughness_texture, SCREEN_UV).rgb; // 将[0,1]范围的法线映射回[-1,1] normal normalize(normal * 2.0 - 1.0); // 假设视线方向是(0, 0, 1)屏幕朝外。这是一个简化更准确的需要视图空间向量。 vec3 view_dir vec3(0.0, 0.0, 1.0); // 计算边缘因子法线与视线方向点积越小夹角越大边缘越明显。 float rim 1.0 - max(dot(normal, view_dir), 0.0); // 应用幂函数使边缘更锐利 rim pow(rim, rim_power); // 应用强度并混合颜色 vec3 rim_contrib rim_color * rim * rim_intensity; vec3 final_color screen_col.rgb rim_contrib; COLOR vec4(final_color, screen_col.a); }这个效果会给物体的轮廓添加一层光晕非常适合卡通渲染或科幻风格。6. 实战案例构建一个可组合的后处理堆栈在实际项目中我们往往需要同时应用多个后处理效果比如先模糊再叠加泛光最后调色。手动管理多个CanvasLayer和ColorRect会很繁琐。一个更好的方法是构建一个可复用的后处理堆栈系统。6.1 设计思路使用SubViewport进行链式处理我们可以利用SubViewport节点来创建一个离屏渲染链。每个后处理效果都渲染到一个独立的SubViewport中其输出作为下一个效果的输入纹理。主场景包含你的3D/2D世界和主相机。后处理根节点一个Node用于组织所有后处理层。效果层每个效果由一个SubViewportContainer包含一个SubViewport和一个ColorRect组成。ColorRect的材质使用前一个SubViewport的纹理作为screen_texture。6.2 实现步骤创建一个新场景根节点为Node命名为PostProcessStack。添加一个SubViewportContainer子节点命名为Effect1_Container。将其锚点设为全矩形。在Effect1_Container下添加一个SubViewport命名为Effect1_Viewport。设置其大小与主窗口一致例如 1920x1080并勾选Transparent Bg如果效果需要透明度。在Effect1_Viewport下添加一个ColorRect锚点全矩形为其创建着色器材质例如高斯模糊水平Pass。复制Effect1_Container重命名为Effect2_Container。在Effect2_Viewport下的ColorRect着色器中你需要将screen_texture绑定到Effect1_Viewport的纹理。这可以通过在着色器中使用uniform sampler2D previous_pass : source_color;并在材质中将其设置为Effect1_Viewport.get_texture()来实现。重复此过程以添加更多效果。6.3 使用脚本动态管理为了更灵活我们可以用脚本动态创建和管理这个堆栈。extends Node class_name PostProcessStack # 预加载你的后处理着色器资源 var blur_horizontal_shader preload(res://shaders/blur_horizontal.gdshader) var blur_vertical_shader preload(res://shaders/blur_vertical.gdshader) var color_grading_shader preload(res://shaders/color_grading.gdshader) var viewport_size: Vector2 func _ready(): # 假设这个脚本挂载在主场景的某个节点上 viewport_size get_viewport().size # 创建第一个效果水平模糊 var blur_h_pass create_effect_layer(BlurHorizontal, blur_horizontal_shader) add_child(blur_h_pass) # 它的输入是主视口 blur_h_pass.get_node(SubViewport/ColorRect).material.set_shader_parameter(screen_texture, get_viewport().get_texture()) # 创建第二个效果垂直模糊 var blur_v_pass create_effect_layer(BlurVertical, blur_vertical_shader) add_child(blur_v_pass) # 它的输入是上一个视口的纹理 blur_v_pass.get_node(SubViewport/ColorRect).material.set_shader_parameter(screen_texture, blur_h_pass.get_node(SubViewport).get_texture()) # 创建第三个效果色彩分级 var color_pass create_effect_layer(ColorGrading, color_grading_shader) add_child(color_pass) color_pass.get_node(SubViewport/ColorRect).material.set_shader_parameter(screen_texture, blur_v_pass.get_node(SubViewport).get_texture()) # 最后将最终效果输出到主屏幕 # 我们需要一个覆盖全屏的ColorRect来显示最后一个视口的内容 var final_display ColorRect.new() final_display.anchors_preset Control.PRESET_FULL_RECT final_display.material ShaderMaterial.new() final_display.material.shader preload(res://shaders/final_display.gdshader) # 一个简单的传递着色器 final_display.material.set_shader_parameter(final_texture, color_pass.get_node(SubViewport).get_texture()) add_child(final_display) func create_effect_layer(name: String, shader: Shader) - SubViewportContainer: var container SubViewportContainer.new() container.name name _Container container.anchors_preset Control.PRESET_FULL_RECT container.size viewport_size var viewport SubViewport.new() viewport.name name _Viewport viewport.size viewport_size viewport.transparent_bg true # 根据效果需要 viewport.render_target_update_mode SubViewport.UPDATE_ALWAYS container.add_child(viewport) var color_rect ColorRect.new() color_rect.name EffectRect color_rect.anchors_preset Control.PRESET_FULL_RECT color_rect.material ShaderMaterial.new() color_rect.material.shader shader viewport.add_child(color_rect) return container这个脚本创建了一个三阶段的后处理链水平模糊 - 垂直模糊 - 色彩分级。每个阶段都渲染到独立的SubViewport并将纹理传递给下一阶段。最后用一个全屏的ColorRect显示最终结果。重要提示使用SubViewport链会显著增加显存占用和渲染开销因为每一层都需要存储一张全屏纹理。务必根据目标平台性能谨慎使用。对于移动端应尽量减少后处理阶段的数量和分辨率可以通过设置SubViewport的size为半分辨率来降低开销。7. 常见问题与性能优化实录在实现屏幕空间后处理的过程中我踩过不少坑。这里总结一些常见问题和优化技巧。7.1 问题排查速查表问题现象可能原因解决方案屏幕全黑或全白screen_texture采样器未正确声明或绑定。检查着色器中uniform sampler2D screen_texture : hint_screen_texture, ...的拼写和提示符。确保使用CanvasLayer或SubViewport的正确纹理进行绑定。效果没有出现ColorRect节点被其他UI或3D场景遮挡。检查CanvasLayer的Layer属性确保其值足够大如100使其渲染在最上层。检查ColorRect的Visibility和Modulate。效果错位或拉伸ColorRect的锚点和尺寸未正确设置为全屏。在检查器中对ColorRect节点使用Layout - Full Rect预设。性能极差卡顿着色器过于复杂或采样次数过多如大半径模糊。使用分离滤波如分离高斯模糊。降低采样次数或使用优化后的采样核。考虑降低SubViewport的分辨率。深度/法线纹理采样出错着色器类型错误或提示符错误。确保着色器类型为canvas_item。深度纹理使用hint_depth_texture法线纹理使用hint_normal_roughness_texture。在主3D场景的WorldEnvironment中确保启用了相应的缓冲区如SSAO、SSIL会生成这些纹理。效果在编辑器预览中正常运行后异常运行时的视口大小与编辑器不同。在_ready()或_process()中动态更新SubViewport的size为get_viewport().size。使用resize信号。7.2 性能优化技巧减少纹理采样这是后处理着色器性能的关键。尽可能重用采样结果使用双线性采样来模拟更多采样点。利用Mipmap进行下采样模糊对于大范围的模糊效果如景深散景可以先将屏幕纹理渲染到一个更低分辨率的SubViewport在这个小纹理上进行模糊计算然后再上采样回屏幕分辨率。这能极大减少像素处理量。避免分支if语句在片段着色器中分支特别是依赖于纹理采样结果的分支会严重降低GPU的并行效率。尽量使用mix()或step()函数进行线性插值或阈值判断。预计算数据像高斯模糊的权重、颜色查找表LUT等不变的数据可以作为Uniform数组或纹理传入着色器避免在着色器中进行复杂计算。按需启用不是所有场景都需要全套后处理。可以通过脚本控制CanvasLayer或SubViewportContainer的visible属性或者动态替换着色器来开关特定效果。选择正确的纹理过滤模式对于需要锐利边缘的效果如像素化、边缘检测使用filter_nearest。对于需要平滑过渡的效果如模糊、色调映射使用filter_linear。7.3 一个实用的性能优化案例半分辨率泛光泛光Bloom通常需要先提取高亮区域然后进行多次模糊。一个常见的优化是使用“乒乓”技术在两个纹理之间交替渲染和半分辨率处理。亮度阈值提取在全分辨率下提取颜色亮度超过某个阈值的像素。下采样将提取的高亮图下采样到1/4或1/8分辨率。模糊在低分辨率下进行多次高斯模糊水平垂直。上采样与混合将模糊后的低分辨率泛光纹理上采样回屏幕分辨率并与原图叠加。通过在半分辨率甚至更低分辨率下进行最耗时的模糊操作可以节省多达75%的像素处理量而对最终视觉效果的影响微乎其微。实现这个效果需要精心设计SubViewport链和对应的着色器是综合运用上述技巧的绝佳练习。它让我深刻体会到屏幕空间特效不仅是艺术更是性能和视觉质量之间精妙平衡的技术。

相关新闻