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

Unity URP屏幕受伤效果:手写Shader实现沉浸式后处理特效 1. 项目概述与核心价值屏幕受伤效果或者更广义的“屏幕空间特效”是提升游戏沉浸感和视觉反馈强度的利器。想象一下在激烈的FPS对战中你被敌人击中视野边缘瞬间泛红、模糊甚至出现裂纹和血迹或者在恐怖游戏中角色受到精神冲击屏幕扭曲、出现重影。这些效果不仅仅是“好看”它们直接、高效地向玩家传递了关键的游戏状态信息生命值降低、受到攻击、处于异常状态。相比于传统的UI血条闪烁这种将信息直接“烙印”在玩家视野上的方式更具冲击力和代入感。后处理Post-Processing是实现这类效果最主流、最高效的技术路径。它的核心思想是在摄像机完成了所有不透明和透明物体的渲染得到一张完整的“屏幕图像”之后再对这张图像进行额外的加工处理。这就像摄影师在拍完照片后用Photoshop进行调色、添加滤镜一样。在Unity中这意味着我们无需修改场景中成千上万个物体的材质只需在渲染流程的最后一步施加一个全局的“魔法”就能改变整个画面的观感。本次我们要实现的“逼真屏幕受伤效果”就是一个典型的后处理应用。它不是一个单一的效果而是一个由多种视觉元素复合而成的系统旨在模拟角色受伤时主观视觉的生理和心理反应。我们将从最基础的屏幕泛红模拟血液上涌、视线充血开始逐步叠加动态模糊模拟意识恍惚、视线失焦、边缘暗角模拟视野收缩、注意力集中以及最复杂的屏幕裂纹效果。整个过程我们将手写一个完整的后处理Shader并集成到Unity的URP通用渲染管线中。选择手写Shader而非完全依赖现成的后处理堆栈V2是为了获得最大的灵活性和控制精度让你能透彻理解每一个像素变化的原理并能随心所欲地定制属于自己游戏的独特“受伤语言”。2. 核心思路与方案设计要实现一个逼真的受伤效果我们不能简单地给屏幕贴一张红色半透明的图。那样会显得非常廉价和出戏。我们需要模拟的是人体在受到创伤时视觉系统可能产生的一系列连锁反应并将它们合理地映射到屏幕后处理这个二维平面上。2.1 效果分层与复合设计我们的效果将分为四个可独立控制的层次它们会以特定的方式和顺序叠加基础色相偏移血色浸润这是最底层、最基础的效果。当受到伤害时我们不是简单覆盖一层红色而是将屏幕图像的整体色相向红色区域偏移同时可能轻微提高对比度和饱和度。这样做的目的是模拟眼球充血或大脑供血变化对色彩感知的影响它改变了整个画面的“色调”而非覆盖一层颜色。径向动态模糊视线失焦受伤的瞬间人可能会感到晕眩视线无法聚焦。我们将以屏幕中心或受击点如果能够获取为原点施加一个径向模糊。离中心越远模糊强度越大。这个模糊应该是动态的在受击瞬间最强然后快速衰减。可动画化的裂纹遮罩视觉损伤这是效果的“实体”部分。我们将使用一张精心绘制的、带有Alpha通道的裂纹纹理。这张纹理不会简单地平铺在屏幕上而是会以程序化的方式控制其显示如淡入、淡出、位置可能与受击方向相关和动画裂纹的延伸、扩散。通过混合模式让裂纹看起来像是“刻”在屏幕玻璃上一样。边缘暗角与色差感官冲击在受到重击时视野可能会变暗、收缩甚至因为冲击导致视觉信号短暂紊乱产生色差红蓝通道偏移。我们会在屏幕边缘添加一个可调节的暗角并在裂纹或高对比度边缘附近模拟微小的色差效果增加不适感和冲击力。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 GraphShader Graph可视化、易上手但对于复杂的、需要精细逐像素操作和自定义缓冲区的后处理效果手写HLSL代码在灵活性和性能优化上更有优势。理解手写流程也是深入图形学的必经之路。2.3 资源准备要点在开始写代码前需要准备好关键资源裂纹纹理一张黑底、白色裂纹的PNG图片。白色部分代表裂纹黑色部分透明。纹理的Wrap Mode应设为ClampFilter Mode设为Bilinear或Trilinear。裂纹的绘制要有粗细、分叉的变化避免规则和重复。噪声纹理一张灰度噪声图用于给裂纹边缘、模糊强度等添加一些随机性和有机感避免效果过于生硬和计算机化。3. 完整实现步骤拆解接下来我们一步步搭建整个系统。请确保你的Unity项目使用的是URP本文以URP 14.x / 2022.3 LTS版本为例。3.1 第一步创建自定义后处理RenderFeature这是连接C#逻辑和Shader的桥梁。在项目中创建两个C#脚本ScreenInjuryRenderPass.cs和ScreenInjuryFeature.cs。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。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); } }在URP Asset的Renderer列表中找到你使用的Renderer如Universal Renderer Data在它的Inspector面板中添加ScreenInjuryFeature。3.2 第二步编写屏幕受伤效果ShaderHLSL这是核心。我们创建一个名为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中。采样屏幕颜色float4 col SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);应用色相偏移模拟充血// 简易方法向红色通道混合并调整饱和度/亮度 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);应用径向动态模糊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或运动模糊优化方案。这里为了清晰展示原理使用了简单循环。叠加裂纹效果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通道做精细混合添加边缘暗角与色差// 暗角 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; }返回最终颜色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 性能优化要点降低采样次数模糊循环中的samples是性能杀手。在移动平台尝试用4方向采样上、下、左、右替代8方向或更多。使用降分辨率渲染对于模糊这类本身就会损失细节的效果完全可以在一半或四分之一分辨率的Render Texture上进行最后再上采样到屏幕分辨率。这能极大减少像素处理量。分Pass渲染将模糊、裂纹、色彩调整等效果拆分成多个Render Pass和Shader变体。这样当_InjuryIntensity很低时可以跳过昂贵的模糊Pass只应用简单的色彩调整。善用LOD为效果设置细节等级。在低端设备上关闭色差降低模糊质量甚至只用简单的屏幕泛红。避免每帧更新所有参数如果某些参数如裂纹纹理、噪声纹理不变只需在初始化时设置一次。5.2 进阶效果拓展基础系统搭建好后可以尝试以下进阶效果让反馈更精准、更丰富受击方向反馈在TakeDamage方法中传入攻击方向Vector3。在Shader中利用这个方向向量来影响效果。非对称模糊让模糊方向偏向受击方向的垂直方向。裂纹生长方向让裂纹纹理的流动或生长方向与受击方向相关。屏幕偏移模拟受击时的头部晃动对屏幕UV做一个短暂的、小幅度的偏移。多层级裂纹与破损准备多张不同严重程度的裂纹纹理。根据累计伤害或单次伤害大小动态切换或混合多张裂纹图实现从细微裂痕到屏幕完全破碎的效果。与游戏状态深度集成不同伤害类型中毒时屏幕泛绿并带有毒性波纹触电时屏幕闪烁蓝白并带有扫描线噪波精神攻击时屏幕扭曲、出现重影。角色状态影响当角色处于“狂暴”状态时受伤效果减弱或变为增强对比度的风格当角色“濒死”时效果变为黑白并带有强烈的心跳式脉冲暗角。自定义渲染纹理混合除了处理摄像机颜色还可以渲染一张单独的“伤害遮罩”纹理比如通过Overlay Draw Call画出受伤区域的精灵在后处理Shader中采样并与之混合实现更复杂的、基于几何体的受伤效果。5.3 调试技巧使用Frame DebuggerUnity的Frame Debugger是查看后处理Pass执行顺序和渲染目标内容的终极工具。确保你的RenderPass在正确的事件点执行并且没有不必要的Clear或Blit操作。参数可视化在ScreenInjuryEffect脚本的Inspector上用[Range]滑块暴露所有关键Shader参数并提供一个测试按钮来模拟受伤。这能极大提高美术和策划的调节效率。分离调试在Shader中可以通过return float4(crackVisibility, 0, 0, 1);这样的方式单独输出某个中间变量如裂纹可见度到屏幕来确认该部分逻辑是否正确。实现一个逼真的屏幕受伤效果是技术逻辑与美术感性的完美结合。它要求你对渲染管线、Shader编程有扎实的理解同时也需要对视觉心理学和玩家体验有敏锐的洞察。从简单的屏幕泛红开始逐步叠加层次精心调节每一个参数你就能为你的游戏创造出极具辨识度和情绪张力的视觉语言。记住最好的效果是那些玩家可能没有刻意注意到但一旦失去就会觉得游戏“不对劲”的效果。