Unity URP屏幕受伤效果:手写Shader实现沉浸式后处理特效

📅 2026/7/22 9:22:13 👁️ 阅读次数 📝 编程学习
Unity URP屏幕受伤效果:手写Shader实现沉浸式后处理特效

1. 项目概述与核心价值

屏幕受伤效果,或者更广义的“屏幕空间特效”,是提升游戏沉浸感和视觉反馈强度的利器。想象一下,在激烈的FPS对战中,你被敌人击中,视野边缘瞬间泛红、模糊,甚至出现裂纹和血迹;或者在恐怖游戏中,角色受到精神冲击,屏幕扭曲、出现重影。这些效果不仅仅是“好看”,它们直接、高效地向玩家传递了关键的游戏状态信息:生命值降低、受到攻击、处于异常状态。相比于传统的UI血条闪烁,这种将信息直接“烙印”在玩家视野上的方式,更具冲击力和代入感。

后处理(Post-Processing)是实现这类效果最主流、最高效的技术路径。它的核心思想是在摄像机完成了所有不透明和透明物体的渲染,得到一张完整的“屏幕图像”之后,再对这张图像进行额外的加工处理。这就像摄影师在拍完照片后,用Photoshop进行调色、添加滤镜一样。在Unity中,这意味着我们无需修改场景中成千上万个物体的材质,只需在渲染流程的最后一步,施加一个全局的“魔法”,就能改变整个画面的观感。

本次我们要实现的“逼真屏幕受伤效果”,就是一个典型的后处理应用。它不是一个单一的效果,而是一个由多种视觉元素复合而成的系统,旨在模拟角色受伤时主观视觉的生理和心理反应。我们将从最基础的屏幕泛红(模拟血液上涌、视线充血)开始,逐步叠加动态模糊(模拟意识恍惚、视线失焦)、边缘暗角(模拟视野收缩、注意力集中)以及最复杂的屏幕裂纹效果。整个过程,我们将手写一个完整的后处理Shader,并集成到Unity的URP(通用渲染管线)中。选择手写Shader而非完全依赖现成的后处理堆栈(V2),是为了获得最大的灵活性和控制精度,让你能透彻理解每一个像素变化的原理,并能随心所欲地定制属于自己游戏的独特“受伤语言”。

2. 核心思路与方案设计

要实现一个逼真的受伤效果,我们不能简单地给屏幕贴一张红色半透明的图。那样会显得非常廉价和出戏。我们需要模拟的是人体在受到创伤时,视觉系统可能产生的一系列连锁反应,并将它们合理地映射到屏幕后处理这个二维平面上。

2.1 效果分层与复合设计

我们的效果将分为四个可独立控制的层次,它们会以特定的方式和顺序叠加:

  1. 基础色相偏移(血色浸润):这是最底层、最基础的效果。当受到伤害时,我们不是简单覆盖一层红色,而是将屏幕图像的整体色相向红色区域偏移,同时可能轻微提高对比度和饱和度。这样做的目的是模拟眼球充血或大脑供血变化对色彩感知的影响,它改变了整个画面的“色调”,而非覆盖一层颜色。
  2. 径向动态模糊(视线失焦):受伤的瞬间,人可能会感到晕眩,视线无法聚焦。我们将以屏幕中心或受击点(如果能够获取)为原点,施加一个径向模糊。离中心越远,模糊强度越大。这个模糊应该是动态的,在受击瞬间最强,然后快速衰减。
  3. 可动画化的裂纹遮罩(视觉损伤):这是效果的“实体”部分。我们将使用一张精心绘制的、带有Alpha通道的裂纹纹理。这张纹理不会简单地平铺在屏幕上,而是会以程序化的方式控制其显示(如淡入、淡出)、位置(可能与受击方向相关)和动画(裂纹的延伸、扩散)。通过混合模式,让裂纹看起来像是“刻”在屏幕玻璃上一样。
  4. 边缘暗角与色差(感官冲击):在受到重击时,视野可能会变暗、收缩,甚至因为冲击导致视觉信号短暂紊乱,产生色差(红蓝通道偏移)。我们会在屏幕边缘添加一个可调节的暗角,并在裂纹或高对比度边缘附近模拟微小的色差效果,增加不适感和冲击力。

2.2 技术方案选型:URP与全屏Shader

在Unity中实现后处理主要有几种方式:传统的OnRenderImage、内置渲染管线的后处理堆栈、SRP(可编程渲染管线)下的自定义后处理。对于新项目,URP是毫无疑问的首选。它性能更好,架构更现代,并且我们的效果需要深度纹理等数据,URP提供了标准化的获取方式。

我们将创建一个MonoBehaviour脚本(如ScreenInjuryEffect)来管理受伤状态(生命值、受击强度、持续时间等)。这个脚本会计算一系列float参数(如_InjuryIntensity,_BlurStrength,_CrackProgress),并将它们传递给一个我们自定义的Fullscreen Shader Graph或手写的HLSL Shader。我们选择手写一个Unlit Shader,因为它能给我们最大的控制权。然后,通过URP的RenderFeature(渲染特性)系统,在渲染管线的正确阶段(通常在AfterRenderingPostProcessing之后)插入一个ScriptableRenderPass,来执行我们的全屏绘制。

注意:为什么不直接用Shader Graph?Shader Graph可视化、易上手,但对于复杂的、需要精细逐像素操作和自定义缓冲区的后处理效果,手写HLSL代码在灵活性和性能优化上更有优势。理解手写流程也是深入图形学的必经之路。

2.3 资源准备要点

在开始写代码前,需要准备好关键资源:

  • 裂纹纹理:一张黑底、白色裂纹的PNG图片。白色部分代表裂纹,黑色部分透明。纹理的Wrap Mode应设为Clamp,Filter Mode设为BilinearTrilinear。裂纹的绘制要有粗细、分叉的变化,避免规则和重复。
  • 噪声纹理:一张灰度噪声图,用于给裂纹边缘、模糊强度等添加一些随机性和有机感,避免效果过于生硬和计算机化。

3. 完整实现步骤拆解

接下来,我们一步步搭建整个系统。请确保你的Unity项目使用的是URP(本文以URP 14.x / 2022.3 LTS版本为例)。

3.1 第一步:创建自定义后处理RenderFeature

这是连接C#逻辑和Shader的桥梁。

  1. 在项目中创建两个C#脚本:ScreenInjuryRenderPass.csScreenInjuryFeature.cs

  2. ScreenInjuryRenderPass继承自ScriptableRenderPass。它的核心是Execute方法,在这里我们将使用CommandBuffer来绘制一个全屏四边形,并调用我们的受伤效果Shader。

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (_material == null || _injuryEffect == null || !_injuryEffect.IsActive) return; CommandBuffer cmd = CommandBufferPool.Get("ScreenInjuryEffect"); // 设置Shader所需的全局属性(_InjuryIntensity, _CrackTex, _CrackParams等) // 这些属性值来自 _injuryEffect 脚本 _material.SetFloat("_InjuryIntensity", _injuryEffect.CurrentIntensity); _material.SetTexture("_CrackTex", _injuryEffect.CrackTexture); // ... 设置其他参数 // 执行全屏绘制 Blitter.BlitCameraTexture(cmd, renderingData.cameraData.renderer.cameraColorTargetHandle, renderingData.cameraData.renderer.cameraColorTargetHandle, _material, 0); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); }

    实操心得:使用CommandBufferPool.Get/Release来管理CommandBuffer对象,避免每帧创建GC Alloc。名称“ScreenInjuryEffect”有助于在Frame Debugger中清晰识别该Pass。

  3. ScreenInjuryFeature继承自ScriptableRendererFeature。它在Create方法中实例化RenderPass,在AddRenderPasses方法中将其插入到渲染管线中。

    public override void Create() { _scriptablePass = new ScreenInjuryRenderPass(); _scriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; // 关键:在标准后处理之后执行 } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (renderingData.cameraData.cameraType == CameraType.Game) { renderer.EnqueuePass(_scriptablePass); } }
  4. 在URP Asset的Renderer列表中找到你使用的Renderer(如Universal Renderer Data),在它的Inspector面板中,添加ScreenInjuryFeature

3.2 第二步:编写屏幕受伤效果Shader(HLSL)

这是核心。我们创建一个名为ScreenInjury.shader的文件。

3.2.1 属性与变量定义Properties块和HLSL代码中声明我们需要的参数:

Properties { _MainTex ("Screen Texture", 2D) = "white" {} _CrackTex ("Crack Texture", 2D) = "black" {} _NoiseTex ("Noise Texture", 2D) = "gray" {} _InjuryIntensity ("Injury Intensity", Range(0, 1)) = 0.0 _BloodTint ("Blood Tint Color", Color) = (1, 0.1, 0.1, 1) _BlurStrength ("Blur Strength", Range(0, 0.1)) = 0.02 _CrackScale ("Crack Scale", Float) = 1.0 _CrackProgress ("Crack Progress", Range(0, 1)) = 0.5 _VignetteIntensity ("Vignette Intensity", Range(0, 1)) = 0.5 _ChromaticAberration ("Chromatic Aberration", Range(0, 0.05)) = 0.01 }

对应的HLSL变量也需要定义。

3.2.2 顶点与片段着色器顶点着色器负责传递标准的全屏四边形UV。重点在片段着色器frag中。

  1. 采样屏幕颜色float4 col = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
  2. 应用色相偏移(模拟充血)
    // 简易方法:向红色通道混合,并调整饱和度/亮度 float bloodInfluence = _InjuryIntensity * 0.7; // 系数控制影响程度 col.rgb = lerp(col.rgb, col.rgb * _BloodTint.rgb, bloodInfluence); // 可以增加一个简单的饱和度/对比度调整,使颜色更“刺眼” float avg = (col.r + col.g + col.b) / 3.0; col.rgb = lerp(float3(avg, avg, avg), col.rgb, 1.0 + bloodInfluence * 0.3);
  3. 应用径向动态模糊
    float2 centerVector = i.uv - float2(0.5, 0.5); float distanceFromCenter = length(centerVector); float blur = _BlurStrength * _InjuryIntensity * saturate(distanceFromCenter * 2.0); // 边缘模糊更强 float4 blurredCol = col; int samples = 8; // 采样数,影响质量和性能 for(int j = 0; j < samples; j++) { float angle = (float)j / (float)samples * PI * 2.0; float2 offset = float2(cos(angle), sin(angle)) * blur; blurredCol += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv + offset); } blurredCol /= (samples + 1); col = lerp(col, blurredCol, saturate(_InjuryIntensity * 2.0)); // 混合原始和模糊结果

    注意事项:循环模糊在移动端可能开销较大。实际项目中,可以考虑使用更高效的高斯模糊(分两Pass)或运动模糊优化方案。这里为了清晰展示原理使用了简单循环。

  4. 叠加裂纹效果
    float2 crackUV = i.uv * _CrackScale; // 可以添加一些基于噪声的UV偏移,让裂纹看起来更自然 float2 noiseOffset = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, i.uv * 3.0).rg * 0.02; crackUV += noiseOffset; float4 crackSample = SAMPLE_TEXTURE2D(_CrackTex, sampler_CrackTex, crackUV); float crackValue = crackSample.r; // 使用_CrackProgress控制裂纹的显示范围(从中心扩散或淡入) float crackVisibility = smoothstep(_CrackProgress - 0.2, _CrackProgress, crackValue); crackVisibility *= _InjuryIntensity; // 整体强度控制 // 裂纹颜色(如暗红色或黑色)与屏幕颜色混合 float3 crackColor = lerp(float3(0.1, 0, 0), float3(0.3, 0, 0), crackValue); col.rgb = lerp(col.rgb, crackColor, crackVisibility * crackSample.a); // 使用纹理的Alpha通道做精细混合
  5. 添加边缘暗角与色差
    // 暗角 float vignette = 1.0 - distanceFromCenter * _VignetteIntensity * _InjuryIntensity; vignette = saturate(vignette * vignette); // 二次方曲线使过渡更平滑 col.rgb *= vignette; // 简易色差(仅在裂纹或高亮边缘附近) if(crackVisibility > 0.1 || _InjuryIntensity > 0.8) { float2 redOffset = centerVector * _ChromaticAberration * _InjuryIntensity; float2 blueOffset = -redOffset; col.r = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv + redOffset).r; col.b = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv + blueOffset).b; }
  6. 返回最终颜色return float4(col.rgb, 1.0);

3.3 第三步:创建效果管理器C#脚本

创建ScreenInjuryEffect.cs,挂载到主摄像机上。

public class ScreenInjuryEffect : MonoBehaviour { [SerializeField] private Texture2D _crackTexture; [SerializeField] private float _maxIntensity = 1.0f; [SerializeField] private float _fadeOutSpeed = 0.5f; private float _currentIntensity = 0f; private float _targetIntensity = 0f; // 供RenderPass访问的属性 public bool IsActive => _currentIntensity > 0.01f; public float CurrentIntensity => _currentIntensity; public Texture2D CrackTexture => _crackTexture; void Update() { // 平滑地向目标强度过渡 _currentIntensity = Mathf.MoveTowards(_currentIntensity, _targetIntensity, Time.deltaTime * _fadeOutSpeed); // 可以在这里将_currentIntensity等参数通过Shader.SetGlobalFloat传递给Shader Shader.SetGlobalFloat("_InjuryIntensity", _currentIntensity); // 也可以计算并传递其他动态参数,如基于时间的裂纹进度 float crackProgress = Mathf.Clamp01(_currentIntensity * 1.5f); Shader.SetGlobalFloat("_CrackProgress", crackProgress); } // 外部调用此方法来触发受伤效果 public void TakeDamage(float damageAmount) { // 伤害量映射到强度,可以是非线性曲线 _targetIntensity = Mathf.Clamp01(_targetIntensity + damageAmount * 0.3f); // 可以在这里触发音效、相机震动等 } public void Heal(float healAmount) { _targetIntensity = Mathf.Max(0, _targetIntensity - healAmount * 0.5f); } }

这个脚本管理着效果的生命周期。TakeDamage方法可以由玩家的生命值系统调用。Update中平滑插值强度值,避免效果的突变,并负责将参数传递给Shader(这里用了全局属性,RenderPass中设置亦可)。

4. 效果参数详解与美术调优

Shader写好了,但要让效果“逼真”,大部分功夫在参数调节上。这里提供一个详细的参数调节指南。

4.1 核心强度参数_InjuryIntensity

这是所有效果的“总开关”和“强度系数”。它应该是一个在0到1之间变化的值。

  • 映射关系:不要简单地将玩家生命值百分比映射到强度。应该使用一个曲线。例如,生命值在70%以上时,强度为0;生命值低于30%时,强度急剧上升至1。这符合“重伤才视线模糊”的认知。
  • 动态变化:当受到单次重击时,强度应该瞬间跳升到一个较高值(如0.7),然后快速衰减(如2秒内衰减到0.3),再随着生命值低而维持在一个基础值上。这模拟了“冲击”和“持续伤痛”的区别。

4.2 血色与色彩参数

  • _BloodTint:不要用纯红色(1,0,0)。尝试(1, 0.1, 0.1)(0.9, 0.2, 0.2),带一点暗色调会更真实。
  • 饱和度与对比度:在着色器中,受伤时轻微提高对比度和饱和度,能让画面看起来更“锐利”和“紧张”,与模糊效果形成感官对比。

4.3 模糊参数_BlurStrength

  • 基础值:通常很小,0.01到0.05之间。值太大会让玩家完全看不清,影响游戏性。
  • 径向控制:确保模糊强度从屏幕中心向边缘增加。可以用distanceFromCenter的平方来加强这种关系。
  • 性能:移动端务必减少采样次数(samples)。4次或6次采样配合一个稍大的blur值,可能比8次采样效果更好且更快。

4.4 裂纹效果参数

  • _CrackTex:纹理质量是关键。裂纹要有细节,边缘要有破碎感。可以使用Substance Designer或PS手绘。
  • _CrackScale:缩放裂纹纹理。1.0意味着裂纹纹理覆盖整个屏幕。小于1会让裂纹更大、更稀疏;大于1会让裂纹更密集、更细碎。
  • _CrackProgress:这是实现裂纹“生长”动画的关键。通过从0到1的变化,配合smoothstep函数,可以控制裂纹从无到有、从中心向外扩散的过程。
  • 混合模式:我们使用了lerp进行混合。更高级的做法是,将裂纹作为遮罩,只让裂纹区域的屏幕颜色变暗、变红,而非完全替换。可以尝试col.rgb *= (1.0 - crackVisibility * 0.5);来让裂纹区域变暗。

4.5 暗角与色差参数

  • _VignetteIntensity:暗角强度。受伤时设为0.3-0.6。注意暗角过渡要平滑(使用smoothstep或幂函数)。
  • _ChromaticAberration:色差强度。这是一个非常敏感的参数,0.005到0.02之间微调即可。过大看起来会像劣质3D电影。最好将其触发条件与高强度受伤或裂纹显示区域绑定。

4.6 噪声的应用

噪声纹理_NoiseTex是让数字效果变得“有机”的魔法粉末。

  • 裂纹UV偏移:用噪声轻微扰动裂纹纹理的采样UV,可以让裂纹边缘产生自然的、不规则的毛刺感,而不是光滑的计算机线条。
  • 强度扰动:可以用噪声值来轻微扰动_InjuryIntensity_BlurStrength,模拟视觉效果的轻微波动,仿佛脉搏跳动或呼吸影响。

5. 性能优化与进阶技巧

一个再好的效果,如果导致帧率下降,也是失败的。后处理是“全屏操作”,每个像素都要执行一遍Shader,必须高度重视性能。

5.1 性能优化要点

  1. 降低采样次数:模糊循环中的samples是性能杀手。在移动平台,尝试用4方向采样(上、下、左、右)替代8方向或更多。
  2. 使用降分辨率渲染:对于模糊这类本身就会损失细节的效果,完全可以在一半或四分之一分辨率的Render Texture上进行,最后再上采样到屏幕分辨率。这能极大减少像素处理量。
  3. 分Pass渲染:将模糊、裂纹、色彩调整等效果拆分成多个Render Pass和Shader变体。这样,当_InjuryIntensity很低时,可以跳过昂贵的模糊Pass,只应用简单的色彩调整。
  4. 善用LOD:为效果设置细节等级。在低端设备上,关闭色差,降低模糊质量,甚至只用简单的屏幕泛红。
  5. 避免每帧更新所有参数:如果某些参数(如裂纹纹理、噪声纹理)不变,只需在初始化时设置一次。

5.2 进阶效果拓展

基础系统搭建好后,可以尝试以下进阶效果,让反馈更精准、更丰富:

  1. 受击方向反馈:在TakeDamage方法中传入攻击方向(Vector3)。在Shader中,利用这个方向向量来影响效果。
    • 非对称模糊:让模糊方向偏向受击方向的垂直方向。
    • 裂纹生长方向:让裂纹纹理的流动或生长方向与受击方向相关。
    • 屏幕偏移:模拟受击时的头部晃动,对屏幕UV做一个短暂的、小幅度的偏移。
  2. 多层级裂纹与破损:准备多张不同严重程度的裂纹纹理。根据累计伤害或单次伤害大小,动态切换或混合多张裂纹图,实现从细微裂痕到屏幕完全破碎的效果。
  3. 与游戏状态深度集成
    • 不同伤害类型:中毒时屏幕泛绿并带有毒性波纹;触电时屏幕闪烁蓝白并带有扫描线噪波;精神攻击时屏幕扭曲、出现重影。
    • 角色状态影响:当角色处于“狂暴”状态时,受伤效果减弱或变为增强对比度的风格;当角色“濒死”时,效果变为黑白并带有强烈的心跳式脉冲暗角。
  4. 自定义渲染纹理混合:除了处理摄像机颜色,还可以渲染一张单独的“伤害遮罩”纹理(比如通过Overlay Draw Call画出受伤区域的精灵),在后处理Shader中采样并与之混合,实现更复杂的、基于几何体的受伤效果。

5.3 调试技巧

  1. 使用Frame Debugger:Unity的Frame Debugger是查看后处理Pass执行顺序和渲染目标内容的终极工具。确保你的RenderPass在正确的事件点执行,并且没有不必要的Clear或Blit操作。
  2. 参数可视化:在ScreenInjuryEffect脚本的Inspector上,用[Range]滑块暴露所有关键Shader参数,并提供一个测试按钮来模拟受伤。这能极大提高美术和策划的调节效率。
  3. 分离调试:在Shader中,可以通过return float4(crackVisibility, 0, 0, 1);这样的方式,单独输出某个中间变量(如裂纹可见度)到屏幕,来确认该部分逻辑是否正确。

实现一个逼真的屏幕受伤效果,是技术逻辑与美术感性的完美结合。它要求你对渲染管线、Shader编程有扎实的理解,同时也需要对视觉心理学和玩家体验有敏锐的洞察。从简单的屏幕泛红开始,逐步叠加层次,精心调节每一个参数,你就能为你的游戏创造出极具辨识度和情绪张力的视觉语言。记住,最好的效果是那些玩家可能没有刻意注意到,但一旦失去就会觉得游戏“不对劲”的效果。