尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity屏幕受伤特效:从Shader原理到实战实现

Unity屏幕受伤特效:从Shader原理到实战实现 1. 项目概述从“受伤”到“沉浸感”的视觉桥梁在动作、射击或生存类游戏中当角色受到攻击时除了生命值HP的减少和音效的反馈最直观、最能调动玩家情绪的莫过于屏幕视觉特效的变化。一个设计精良的屏幕受伤特效能瞬间将玩家从“旁观者”拉入“参与者”的角色让每一次受击都变得真实可感。它不仅仅是画面边缘的一抹红色而是一个综合了色彩偏移、模糊、扭曲、血迹叠加甚至画面撕裂的复合视觉系统。今天我们就来深入探讨如何在Unity中不依赖昂贵的后处理资源包而是亲手编写Shader实现一套从原理到实践都清晰可控的逼真屏幕受伤特效。这个特效的核心目标是模拟人体在受到创伤时视觉系统可能产生的生理和心理反应视线模糊、注意力集中在伤口屏幕中心向受击点偏移、色彩感知变化偏向红色调以及可能的视野遮挡血迹。我们将完全通过编写自定义的Image Effect Shader后处理着色器来实现这意味着它能以极低的性能开销作用于整个摄像机渲染画面之上兼容从移动端到PC的各个平台。通过本篇文章你将不仅获得一套可直接复用的完整Shader代码更能透彻理解其背后的图形学原理和设计思路从而具备根据自己游戏风格定制专属受伤反馈的能力。2. 核心思路与方案选型为何选择全屏后处理Shader在Unity中实现屏幕特效通常有几种路径使用粒子系统在UI层叠加、修改摄像机的后处理堆栈如URP/HDRP的Volume、或者编写传统的全屏后处理Shader。对于受伤特效这种需要全局、动态、且与场景内容深度交互的效果全屏后处理Shader是最灵活、性能可控且效果强大的选择。2.1 为何是全屏后处理首先受伤特效影响的是玩家的整个“视野”即摄像机看到的一切。粒子系统虽然能模拟飞溅的血滴但难以实现全局的色彩扭曲、模糊等效果。其次后处理Shader在GPU上运行对每个屏幕像素进行一次处理效率极高。最后它允许我们访问渲染完成后的最终图像缓冲区并基于一些自定义参数如受伤强度、受击点位置进行数学运算实现高度定制化的效果。2.2 传统Built-in管线 vs SRP (URP/HDRP)我们的方案将主要基于Unity的传统Built-in渲染管线。原因在于其通用性最强无需绑定特定的渲染管线代码结构清晰便于理解原理。在Built-in管线中我们通过为摄像机挂载一个脚本该脚本调用OnRenderImage函数将源渲染纹理source和我们编写的Shader材质Material传递给Graphics.Blit函数从而应用特效。理解这个过程后迁移到URP编写RenderFeature或HDRP的思路是相通的主要是API的差异。2.3 特效构成分解一个逼真的受伤特效不是单一效果而是多个效果的有机叠加。我们将它拆解为以下几个可独立控制也可组合的核心层色彩偏移与染色模拟视觉充血将画面整体或局部向红色系偏移并可能降低饱和度。径向模糊与视觉扭曲模拟因疼痛或冲击导致的视线模糊、失焦效果从受击点向外辐射。动态血迹贴图叠加在屏幕随机位置或受击点方向叠加半透明的血迹纹理增加细节。屏幕边缘暗角与脉动受伤时视野收窄边缘变暗并且伴随心跳或疼痛的脉搏有节奏地明暗变化。画面抖动可选强烈的受击可伴随短暂的摄像机抖动这部分通常在脚本中控制摄像机Transform但可与Shader参数联动。我们将编写一个统一的Shader通过一组可调节的参数来混合这些子效果。3. Shader核心原理与代码结构拆解接下来我们深入到Shader代码内部。一个完整的后处理Shader通常包含一个ShaderLab文件.shader里面定义了属性Properties、子着色器SubShader和通道Pass。核心算法在片段着色器fragment shader中实现。3.1 属性Properties块定义这是Shader的对外接口允许我们在Unity材质检视器中调节参数也允许脚本通过Material.SetFloat等方法动态修改。Properties { _MainTex (Base (RGB), 2D) white {} // 源屏幕图像 // 1. 全局色彩控制 _DamageIntensity (受伤强度, Range(0, 1)) 0.0 // 核心驱动参数 _RedShift (红色偏移强度, Range(0, 1)) 0.5 _Saturation (饱和度, Range(0, 2)) 1.0 _VignetteIntensity (暗角强度, Range(0, 2)) 0.5 // 2. 径向模糊控制 _BlurCenter (模糊中心 (屏幕空间XY), Vector) (0.5, 0.5, 0, 0) // 受击点 _BlurRadius (模糊半径, Range(0, 0.1)) 0.02 _BlurSamples (采样次数, Int) 10 // 3. 血迹控制 _BloodTex (血迹贴图 (A), 2D) white {} _BloodScale (血迹缩放, Float) 1.0 _BloodIntensity (血迹强度, Range(0, 1)) 0.3 [HDR] _BloodColor (血迹颜色, Color) (1,0,0,1) // 4. 脉动效果 _PulseSpeed (脉动速度, Float) 2.0 _PulseAmount (脉动幅度, Range(0, 0.5)) 0.1 }3.2 顶点-片段着色器基本结构在SubShader的Pass中我们使用CGPROGRAM编写CG/HLSL代码。v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; // 计算一个从中心(0.5,0.5)到当前uv的向量用于径向效果 o.uvCenterVector v.uv - float2(0.5, 0.5); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 originalColor tex2D(_MainTex, i.uv); fixed4 finalColor originalColor; // 在这里我们将依次叠加各种效果 // finalColor ApplyColorShift(finalColor, i.uv); // finalColor ApplyRadialBlur(finalColor, i.uv); // finalColor ApplyBloodSplatter(finalColor, i.uv); // finalColor ApplyVignetteAndPulse(finalColor, i.uv); // 最后根据_DamageIntensity在原始颜色和特效颜色之间插值 // return lerp(originalColor, finalColor, _DamageIntensity); return finalColor; }注意在实际编写时所有效果应基于同一个原始采样originalColor进行分别计算后再混合而不是层层嵌套finalColor以避免颜色计算误差累积和性能浪费。上面注释中的链式调用仅为逻辑示意。4. 关键效果算法实现详解现在我们来逐一实现frag函数中提到的各个效果函数。4.1 色彩偏移与染色ApplyColorShift这个效果模拟视觉充血。我们不只是简单增加红色而是更科学地调整色相和饱和度。fixed4 ApplyColorShift(fixed4 color, float2 uv) { // 1. 将RGB颜色转换到HSL/HSV空间进行调整更自然但为简化我们使用RGB偏移 // 增加红色通道轻微减少绿色和蓝色通道 float redBoost color.r * (1.0 _RedShift * _DamageIntensity); float greenReduct color.g * (1.0 - _RedShift * _DamageIntensity * 0.5); float blueReduct color.b * (1.0 - _RedShift * _DamageIntensity * 0.3); fixed3 shiftedColor fixed3(redBoost, greenReduct, blueReduct); // 2. 调整饱和度 fixed3 luminance dot(color.rgb, fixed3(0.299, 0.587, 0.114)); // 计算亮度 shiftedColor lerp(luminance, shiftedColor, _Saturation); // 3. 可选根据受伤强度在屏幕边缘加强红色偏移模拟视野边缘充血 float distanceFromCenter length(uv - float2(0.5, 0.5)); float edgeFactor smoothstep(0.2, 0.8, distanceFromCenter); // 边缘因子 shiftedColor.r edgeFactor * _DamageIntensity * 0.2; return fixed4(shiftedColor, color.a); }4.2 径向模糊ApplyRadialBlur径向模糊从受击点_BlurCenter向外发散。其原理是在当前像素点到中心点的方向上进行多次采样并平均。fixed4 ApplyRadialBlur(fixed4 originalColor, float2 uv) { // 如果模糊半径或强度为0直接返回原色以节省性能 if (_BlurRadius 0 || _DamageIntensity 0.1) return originalColor; fixed4 blurredColor originalColor; float2 blurVector (_BlurCenter.xy - uv) * _BlurRadius * _DamageIntensity; float2 sampleUV; float totalWeight 1.0; // 初始权重为1原始采样 for (int j 1; j _BlurSamples; j) { // 从当前点向中心方向偏移采样 sampleUV uv blurVector * (float(j) / float(_BlurSamples - 1)); // 简单的线性加权离原始点越近的采样权重越高可选 float weight 1.0 - (float(j) / float(_BlurSamples)); blurredColor tex2D(_MainTex, sampleUV) * weight; totalWeight weight; } blurredColor / totalWeight; return blurredColor; }实操心得_BlurSamples采样次数是性能与质量的权衡关键。移动端建议控制在5-8次PC端可到10-15次。另外确保sampleUV在0到1之间屏幕空间否则需要对越界的UV进行钳制clamp或包裹wrap处理否则在屏幕边缘会出现拉伸瑕疵。可以使用tex2D(_MainTex, clamp(sampleUV, 0.001, 0.999))来简单处理。4.3 动态血迹叠加ApplyBloodSplatter血迹效果需要一张带有Alpha通道的纹理_BloodTex。为了动态和随机性我们将在脚本中控制血迹的UV偏移和显示。fixed4 ApplyBloodSplatter(fixed4 color, float2 uv) { // 计算血迹UV可以加入随机偏移、旋转和缩放 float2 bloodUV uv * _BloodScale; // 假设脚本通过Material.SetTextureOffset/SetTextureScale控制了血迹位置 fixed4 bloodSample tex2D(_BloodTex, bloodUV); // 只使用血迹贴图的Alpha通道来控制形状颜色用我们定义的_BloodColor fixed3 bloodColor _BloodColor.rgb; float bloodAlpha bloodSample.a * _BloodIntensity * _DamageIntensity; // 使用Alpha混合叠加血迹 color.rgb lerp(color.rgb, bloodColor, bloodAlpha); // 或者使用更真实的叠加模式如正片叠底Multiply // color.rgb lerp(color.rgb, color.rgb * bloodColor, bloodAlpha * 0.5); return color; }4.4 暗角与脉动ApplyVignetteAndPulse暗角Vignette是屏幕边缘变暗的效果。脉动则是让这个暗角或整体亮度随着时间周期性变化。fixed4 ApplyVignetteAndPulse(fixed4 color, float2 uv, float2 centerVector) { // 1. 计算基于到中心距离的暗角因子 float distance length(centerVector) * 2.0; // 映射到0~√2范围我们归一化到0~1.4 float vignette smoothstep(0.0, 1.4 * _VignetteIntensity, distance); vignette 1.0 - vignette; // 中心亮边缘暗 // 2. 计算脉动因子 float pulse sin(_Time.y * _PulseSpeed) * 0.5 0.5; // 映射到0~1 pulse lerp(1.0, 1.0 - _PulseAmount, pulse); // 脉动在1和(1-_PulseAmount)之间 // 3. 结合受伤强度受伤越重暗角越明显脉动影响越大 float effectFactor _DamageIntensity; vignette lerp(1.0, vignette, effectFactor); pulse lerp(1.0, pulse, effectFactor); // 4. 应用效果 color.rgb * vignette * pulse; return color; }5. 完整的Shader代码整合与优化将上述所有函数整合到最终的片段着色器中并注意混合顺序和性能。fixed4 frag (v2f i) : SV_Target { fixed4 originalColor tex2D(_MainTex, i.uv); // 提前计算受伤强度因子用于各个效果 float damageFactor _DamageIntensity; // 【效果计算】建议顺序模糊 - 色彩 - 血迹 - 暗角脉动 // 1. 径向模糊基于原始UV fixed4 colorBlur ApplyRadialBlur(originalColor, i.uv); // 2. 色彩偏移基于模糊后的颜色或原始颜色根据需求 fixed4 colorShifted ApplyColorShift(colorBlur, i.uv); // 3. 血迹叠加 fixed4 colorWithBlood ApplyBloodSplatter(colorShifted, i.uv); // 4. 暗角与脉动 fixed4 finalColor ApplyVignetteAndPulse(colorWithBlood, i.uv, i.uvCenterVector); // 最终用damageFactor在原始和特效间插值实现平滑过渡和关闭 // 注意如果damageFactor为0应完全返回originalColor return lerp(originalColor, finalColor, damageFactor); }5.1 性能优化要点分支判断在Shader开始处如果_DamageIntensity接近于0可以直接返回originalColor跳过所有计算。if (_DamageIntensity 0.001) return originalColor;。但需注意GPU上分支的性能特性在移动端简单的常数比较通常是安全的。采样次数_BlurSamples是性能杀手。可以考虑根据_DamageIntensity动态减少采样次数低强度时用模糊半径更小的3次采样高强度时再用10次。纹理采样确保血迹贴图_BloodTex尺寸合理如512x512并启用Mipmap以减少缓存抖动。精度选择在移动端将fixed4/half4用于颜色计算float用于UV和数学运算以平衡精度和性能。6. C#驱动脚本编写与参数动态控制Shader写好了还需要一个C#脚本来驱动它。这个脚本负责1. 创建材质2. 在OnRenderImage中调用Graphics.Blit3. 提供公共方法给游戏逻辑如PlayerHealth脚本来触发和更新受伤状态。using UnityEngine; [ExecuteInEditMode] // 方便在编辑器预览 [RequireComponent(typeof(Camera))] public class ScreenDamageEffect : MonoBehaviour { public Shader damageShader; private Material _damageMaterial; public Material DamageMaterial { get { if (_damageMaterial null damageShader ! null damageShader.isSupported) { _damageMaterial new Material(damageShader); _damageMaterial.hideFlags HideFlags.HideAndDontSave; } return _damageMaterial; } } [Range(0f, 1f)] public float damageIntensity 0f; public float recoverySpeed 0.5f; // 受伤后恢复速度 public Vector2 hitPoint new Vector2(0.5f, 0.5f); // 受击点屏幕空间0~1 // 血迹参数 public Texture2D bloodTexture; public float bloodScale 1f; public float bloodIntensity 0.3f; public Color bloodColor Color.red; private Vector2 _bloodOffset; // 用于动态移动血迹 void OnEnable() { // 检查Shader和支持 if (damageShader null || !damageShader.isSupported) { enabled false; Debug.LogWarning(ScreenDamageEffect Shader missing or not supported.); return; } // 初始化随机血迹偏移 _bloodOffset new Vector2(Random.Range(0f, 1f), Random.Range(0f, 1f)); } void OnDisable() { if (_damageMaterial ! null) { DestroyImmediate(_damageMaterial); } } // 这是应用后处理的关键函数 void OnRenderImage(RenderTexture source, RenderTexture destination) { if (DamageMaterial ! null damageIntensity 0.001f) { // 1. 将公共变量传递给Shader DamageMaterial.SetFloat(_DamageIntensity, damageIntensity); DamageMaterial.SetVector(_BlurCenter, new Vector4(hitPoint.x, hitPoint.y, 0, 0)); // 2. 动态更新血迹UV偏移例如可以模拟血迹滑落 _bloodOffset.y - Time.deltaTime * 0.1f; DamageMaterial.SetTextureOffset(_BloodTex, _bloodOffset); DamageMaterial.SetTextureScale(_BloodTex, new Vector2(bloodScale, bloodScale)); DamageMaterial.SetTexture(_BloodTex, bloodTexture); DamageMaterial.SetColor(_BloodColor, bloodColor); DamageMaterial.SetFloat(_BloodIntensity, bloodIntensity); // 3. 应用后处理 Graphics.Blit(source, destination, DamageMaterial); } else { // 无特效时直接拷贝 Graphics.Blit(source, destination); } } void Update() { // 自动恢复受伤强度随时间减弱 if (damageIntensity 0) { damageIntensity Mathf.Clamp01(damageIntensity - recoverySpeed * Time.deltaTime); } } // 供外部调用的方法受到伤害 public void TakeDamage(float intensity, Vector2 screenHitPoint) { damageIntensity Mathf.Clamp01(damageIntensity intensity); hitPoint screenHitPoint; // 每次受击可以重置或改变血迹位置 _bloodOffset new Vector2(Random.Range(0f, 1f), Random.Range(0.8f, 1f)); // 新血迹从上方随机位置出现 } }6.1 脚本使用示例在你的玩家生命值脚本中public class PlayerHealth : MonoBehaviour { public float health 100f; public ScreenDamageEffect screenDamage; // 拖拽赋值 public Camera playerCamera; public void ApplyDamage(float damage, Vector3 worldHitPoint) { health - damage; // 将世界坐标的受击点转换为屏幕坐标0~1 Vector3 screenPoint playerCamera.WorldToViewportPoint(worldHitPoint); // 确保点在屏幕内 if (screenPoint.z 0 screenPoint.x 0 screenPoint.x 1 screenPoint.y 0 screenPoint.y 1) { screenDamage.TakeDamage(damage / 100f, new Vector2(screenPoint.x, screenPoint.y)); // 假设满血100伤害比例作为强度 } else { // 如果受击点不在屏幕内使用屏幕中心 screenDamage.TakeDamage(damage / 100f, new Vector2(0.5f, 0.5f)); } } }7. 效果调试、常见问题与性能调优即使代码写完了要获得理想的效果调试至关重要。7.1 调试技巧分层调试在Shader中可以注释掉其他效果只保留一个如只开色彩偏移单独调整其参数观察效果。使用return fixed4(vignette, vignette, vignette, 1);这样的方式直接输出中间变量如暗角因子检查计算是否正确。参数动画在Unity编辑器中可以为damageIntensity等参数创建简单的动画曲线Animation观察从0到1再到0的完整过渡效果是否平滑自然。使用Frame Debugger打开Window - Analysis - Frame Debugger查看OnRenderImagepass是否被正确调用以及Draw Call的开销。7.2 常见问题与解决方案问题现象可能原因解决方案屏幕全黑或全白Shader编译错误或属性未正确绑定检查Console错误日志。确保Shader文件无误且C#脚本中damageShader变量已赋值。在Scene视图选中摄像机查看其ScreenDamageEffect组件的材质球是否创建成功。特效边缘有硬边或拉伸径向模糊的采样UV超出了纹理坐标[0,1]范围在ApplyRadialBlur函数的tex2D采样前对sampleUV进行钳制clamp(sampleUV, 0.001, 0.999)。血迹贴图不显示贴图未赋值或UV计算错误或Alpha通道为纯黑1. 在Inspector中为bloodTexture赋值一张带透明通道的PNG。2. 检查_BloodIntensity和_DamageIntensity是否大于0。3. 在图像软件中确认血迹贴图的Alpha通道有内容。移动设备上帧率下降明显采样次数过多或Shader计算复杂度高1. 降低_BlurSamples到5以下。2. 简化ApplyColorShift中的计算或使用查找表LUT。3. 考虑只在damageIntensity较高时如0.3开启模糊效果。受伤特效不随摄像机旋转/移动_BlurCenter使用的是静态屏幕坐标确保TakeDamage方法中传入的screenHitPoint是动态计算得出的。如果是基于世界坐标的受击点务必每帧或用事件触发时进行转换。多个后处理效果冲突摄像机挂载了多个OnRenderImage脚本后处理效果会按脚本顺序执行。确保你的受伤特效脚本顺序正确。如果需要组合多个后处理可以考虑将它们合并到一个Shader中或者使用Command Buffer进行更精细的控制。7.3 进阶优化与扩展思路基于距离的强度衰减让_DamageIntensity不仅随时间衰减也随玩家与危险源如敌人的距离增加而衰减实现更动态的反馈。不同伤害类型的特效可以定义多个Material或使用Shader变体Keywords为火焰伤害、冰冻伤害、中毒伤害等设计不同的色彩偏移和血迹样式。与角色状态联动当生命值极低时可以叠加一个持续性的、高强度的视觉干扰效果如强烈的噪波、扭曲提示玩家处于危险状态。使用噪声纹理用一张噪声纹理来驱动血迹形状的随机性和模糊效果的失真度可以使效果更加自然有机避免重复感。URP/HDRP适配在URP中你需要创建一个ScriptableRendererFeature和对应的Pass将绘制命令插入到渲染管线中。核心的Shader算法大部分可以复用主要是渲染API的调用方式需要改变。亲手实现一个完整的屏幕受伤特效远不止是复制粘贴代码。从理解后处理管线到设计色彩与模糊算法再到用脚本驱动和调试每一步都加深了你对Unity渲染和游戏反馈设计的理解。最让我有成就感的一刻不是在Shader中调出一个漂亮的红色渐变而是在游戏测试中被敌人击中时屏幕那一下恰到好处的反馈让我本能地感到紧张并做出闪避动作——那才是特效设计的终极目标服务于游戏体验增强玩家的沉浸感。你可以从本文的代码框架出发大胆调整参数甚至加入新的效果层比如屏幕裂纹、色差等创造出独一无二的视觉语言。
返回列表