文章目录
-
- [0. 效果预览](#0. 效果预览)
- [1. 原理简述](#1. 原理简述)
- [2. 功能点](#2. 功能点)
- [3. 完整实现](#3. 完整实现)
-
- [3.1 完整 Shader](#3.1 完整 Shader)
- [3.2 C# Renderer Feature](# Renderer Feature)
- [4. URP Renderer Asset 设置](#4. URP Renderer Asset 设置)
-
- [4.1 找到 URP Renderer Asset](#4.1 找到 URP Renderer Asset)
- [4.2 启用 Opaque Texture(可选)](#4.2 启用 Opaque Texture(可选))
- [5. 使用方法](#5. 使用方法)
- [6. 参数说明](#6. 参数说明)
-
- [Shader 参数](#Shader 参数)
- [C# Feature 参数](# Feature 参数)
- [7. 变体与扩展](#7. 变体与扩展)
-
- [变体 1:爆炸冲击波(径向扩散)](#变体 1:爆炸冲击波(径向扩散))
- [变体 2:局部区域扭曲(遮罩控制)](#变体 2:局部区域扭曲(遮罩控制))
- [变体 3:色差扭曲(Chromatic Aberration)](#变体 3:色差扭曲(Chromatic Aberration))
- [8. 常见问题](#8. 常见问题)
- [9. 性能建议](#9. 性能建议)
0. 效果预览

屏幕空间扭曲(Screen Space Distortion)是一种全屏后处理效果:用噪声图偏移屏幕 UV,让整个画面产生热浪、冲击波、迷幻折射等视觉扭曲。与 UV 扭曲 Shader 不同,它作用于整个屏幕,不依赖具体物体的 UV,适合做爆炸冲击波、传送门边缘、中毒/眩晕状态等全屏特效。
1. 原理简述
屏幕空间扭曲的本质:用噪声图偏移屏幕 UV,再用偏移后的 UV 重新采样屏幕颜色缓冲,让整个画面"错位"。
核心公式:
hlsl
float2 screenUV = input.texcoord; // 屏幕 UV [0,1]
float2 noise = SAMPLE_TEXTURE2D(_NoiseTex, ..., uv).rg * 2.0 - 1.0; // [-1,1]
float2 distortedUV = screenUV + noise * _Strength; // 偏移后的屏幕 UV
half4 col = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, distortedUV); // 重采样
与普通 UV 扭曲的区别:
| UV 扭曲 | 屏幕空间扭曲 | |
|---|---|---|
| 作用范围 | 单个物体的贴图 UV | 整个屏幕 |
| 采样目标 | 物体自身贴图 | 屏幕颜色缓冲 |
| 典型用途 | 水面、护盾表面 | 爆炸冲击波、全屏眩晕 |
| 需要 C# 配合 | 否 | 是(ScriptableRendererFeature) |
URP 实现思路:
URP 中全屏后处理需要通过 ScriptableRendererFeature + ScriptableRenderPass 注入渲染管线。关键在于两步 Blit:
- 在不透明渲染完成后,把当前颜色缓冲复制到一张临时 RT(
_tempRT) - 用扭曲 Shader 把
_tempRT作为输入(即_BlitTexture),输出扭曲结果写回颜色缓冲
为什么要两步?
Blitter.BlitCameraTexture不能把同一张 RT 既作为输入又作为输出,必须先 copy 到临时 RT,再从临时 RT 读取并写回。
2. 功能点
- 全屏噪声扭曲:用噪声图偏移屏幕 UV,重采样颜色缓冲
- 动态滚动:噪声 UV 随时间滚动,产生流动感
- 强度可调 :
_Strength控制扭曲幅度,支持运行时动态修改 - 噪声缩放 :
_NoiseScale控制扭曲纹理的粗细 - 边缘衰减:屏幕边缘扭曲强度渐变为 0,避免画面边缘撕裂感
- C# 运行时控制:通过脚本动态修改材质参数(如爆炸时瞬间拉高再衰减)
3. 完整实现
本效果由两个文件组成:
| 文件 | 职责 |
|---|---|
ScreenDistortion_URP.shader |
读取 _BlitTexture,计算噪声偏移,输出扭曲结果 |
ScreenDistortionFeature.cs |
在不透明渲染后截取屏幕、执行两步 Blit |
3.1 完整 Shader
hlsl
Shader "Custom/ScreenDistortion_URP"
{
Properties
{
_NoiseTex ("Noise Texture (RG)", 2D) = "gray" {}
_Strength ("Distort Strength", Range(0, 0.05)) = 0.015 // 扭曲强度,建议不超过 0.05
_NoiseScale ("Noise Scale", Float) = 2.0 // 噪声缩放,越大纹理越细碎
_Speed ("Scroll Speed (XY)", Vector) = (0.1, 0.07, 0, 0) // 噪声滚动速度
_EdgeFade ("Edge Fade", Range(0, 1)) = 0.1 // 边缘衰减范围,0 = 不衰减
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline" }
Pass
{
Name "ScreenDistortion"
ZWrite Off
ZTest Always
Cull Off
Blend Off
HLSLPROGRAM
#pragma vertex Vert
#pragma fragment frag
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
// Blit.hlsl 提供:Vert 顶点函数、Varyings 结构体、_BlitTexture(屏幕颜色纹理)
// C# Feature 执行两步 Blit 时,会自动将临时 RT 绑定为 _BlitTexture
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"
TEXTURE2D(_NoiseTex); SAMPLER(sampler_NoiseTex);
CBUFFER_START(UnityPerMaterial)
float4 _NoiseTex_ST;
float _Strength;
float _NoiseScale;
float4 _Speed;
float _EdgeFade;
CBUFFER_END
half4 frag(Varyings input) : SV_Target
{
// input.texcoord 是 [0,1] 屏幕 UV,由 Blit.hlsl 的 Vert 自动计算
float2 screenUV = input.texcoord;
// ---- 采样噪声,计算偏移量 ----
float2 noiseUV = screenUV * _NoiseScale + _Time.y * _Speed.xy;
// RG 两通道映射到 [-1, 1],分别驱动 X/Y 方向偏移
float2 noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, noiseUV).rg * 2.0 - 1.0;
// ---- 边缘衰减:靠近屏幕边缘时扭曲渐弱,避免边缘撕裂 ----
float2 edgeDist = min(screenUV, 1.0 - screenUV);
float edgeMask = saturate(min(edgeDist.x, edgeDist.y) / max(_EdgeFade, 0.001));
// ---- 偏移屏幕 UV,重采样屏幕颜色 ----
float2 distortedUV = saturate(screenUV + noise * _Strength * edgeMask);
// _BlitTexture 由 C# Feature 执行 Blitter.BlitCameraTexture 时自动绑定
return SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, distortedUV);
}
ENDHLSL
}
}
}
3.2 C# Renderer Feature
csharp
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
/// <summary>
/// 屏幕空间扭曲后处理 Renderer Feature
/// 挂到 URP Renderer Asset 的 Renderer Feature 列表中
/// </summary>
public class ScreenDistortionFeature : ScriptableRendererFeature
{
[System.Serializable]
public class Settings
{
[Tooltip("扭曲材质(使用 Custom/ScreenDistortion_URP Shader)")]
public Material material;
[Tooltip("注入点,AfterRenderingOpaques = 扭曲不透明物体后的画面")]
public RenderPassEvent injectionPoint = RenderPassEvent.AfterRenderingOpaques;
}
public Settings settings = new Settings();
private DistortionPass _pass;
public override void Create()
{
_pass = new DistortionPass(settings);
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (settings.material == null) return;
if (renderingData.cameraData.cameraType != CameraType.Game &&
renderingData.cameraData.cameraType != CameraType.SceneView)
return;
renderer.EnqueuePass(_pass);
}
private class DistortionPass : ScriptableRenderPass
{
private readonly Settings _settings;
private RTHandle _tempRT;
public DistortionPass(Settings settings)
{
_settings = settings;
renderPassEvent = settings.injectionPoint;
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
var descriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
descriptor.msaaSamples = 1;
// 按当前相机分辨率分配临时 RT
RenderingUtils.ReAllocateIfNeeded(
ref _tempRT,
descriptor,
FilterMode.Bilinear,
TextureWrapMode.Clamp,
name: "_DistortionTemp"
);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (_tempRT == null || _settings.material == null) return;
// 必须在 Execute 内获取 handle,此时相机已开始渲染,handle 非 null
var source = renderingData.cameraData.renderer.cameraColorTargetHandle;
if (source == null) return;
var cmd = CommandBufferPool.Get("ScreenDistortion");
try
{
// 第一步:把当前颜色缓冲复制到临时 RT
Blitter.BlitCameraTexture(cmd, source, _tempRT);
// 第二步:用扭曲 Shader 把临时 RT(_BlitTexture)写回颜色缓冲
Blitter.BlitCameraTexture(cmd, _tempRT, source, _settings.material, 0);
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
}
finally
{
CommandBufferPool.Release(cmd);
}
}
public override void OnCameraCleanup(CommandBuffer cmd) { }
public void Dispose() => _tempRT?.Release();
}
protected override void Dispose(bool disposing)
{
if (disposing) _pass?.Dispose();
base.Dispose(disposing);
_pass = null;
}
}
4. URP Renderer Asset 设置
4.1 找到 URP Renderer Asset
Edit → Project Settings → Graphics- 找到
Default Render Pipeline字段,点击右侧定位图标,Project 窗口高亮对应的 URP Asset

- 选中 URP Asset 后,在 Inspector 的
Rendering分组下,找到Renderer List,点击其中的 Renderer 进入 URP Renderer Asset

4.2 启用 Opaque Texture(可选)
在 URP Asset → Rendering 下勾选 Opaque Texture 。本效果不强制要求(Shader 读的是 _BlitTexture 而非 _CameraOpaqueTexture),但开启后其他效果也可以使用场景颜色。
5. 使用方法
-
创建 Shader 文件 :新建
.shader文件,粘贴 3.1 节代码,保存为ScreenDistortion_URP.shader -
创建 C# 脚本 :新建
.cs文件,粘贴 3.2 节代码,保存为ScreenDistortionFeature.cs,等待 Unity 编译完成 -
创建材质 :新建材质,Shader 选择
Custom/ScreenDistortion_URP,拖入噪声贴图 -
添加自定义 Renderer Feature:
- 选中 URP Renderer Asset(
New Universal Render Pipeline Asset_Renderer) - Inspector →
Add Renderer Feature→ 选择Screen Distortion Feature

- 选中 URP Renderer Asset(
-
配置 Feature:
Material槽:拖入步骤 3 创建的材质Injection Point:保持After Rendering Opaques(默认值)
-
进入 Play Mode,效果立即生效

6. 参数说明
Shader 参数
| 参数 | 类型 | 范围 / 默认值 | 说明 |
|---|---|---|---|
_NoiseTex |
2D | gray | 噪声贴图,RG 两通道分别控制 X/Y 方向偏移 |
_Strength |
Float | 0 ~ 0.05 / 0.015 | 扭曲强度,超过 0.05 画面会明显撕裂 |
_NoiseScale |
Float | 1 ~ 10 / 2.0 | 噪声缩放,越大纹理越细碎 |
_Speed |
Vector | (0.1, 0.07, 0, 0) | 噪声 UV 滚动速度,XY 分量分别控制两个方向 |
_EdgeFade |
Float | 0 ~ 1 / 0.1 | 边缘衰减范围,0 = 不衰减,0.1 = 屏幕边缘 10% 区域渐变到无扭曲 |
C# Feature 参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
Material |
Material | 无 | 扭曲材质,必须填,否则 Feature 自动跳过 |
Injection Point |
RenderPassEvent | AfterRenderingOpaques | 扭曲插入时机,通常保持默认 |
7. 变体与扩展
变体 1:爆炸冲击波(径向扩散)

冲击波从屏幕某点向外扩散,而不是全屏均匀扭曲。核心改动:用距离中心点的距离控制扭曲强度,并用 C# 驱动中心点位置和强度随时间衰减。
hlsl
// 在 frag 中替换强度计算部分
float2 center = _WaveCenter; // C# 传入的冲击波中心(屏幕 UV)
float dist = distance(screenUV, center); // 到中心的距离
float waveMask = smoothstep(_WaveRadius, 0.0, dist); // 距离中心越近,扭曲越强
float strength = _Strength * waveMask * edgeMask;
C# 脚本在爆炸时调用:
csharp
// 将世界坐标转为屏幕 UV,传给材质
Vector3 screenPos = Camera.main.WorldToViewportPoint(explosionWorldPos);
material.SetVector("_WaveCenter", new Vector4(screenPos.x, screenPos.y, 0, 0));
// 用协程或 DOTween 驱动 _Strength 从峰值衰减到 0
变体 2:局部区域扭曲(遮罩控制)
只扭曲屏幕特定区域(如传送门内部),用一张遮罩贴图控制哪些像素参与扭曲:
hlsl
// 采样遮罩贴图,只在遮罩白色区域应用扭曲
float mask = SAMPLE_TEXTURE2D(_MaskTex, sampler_MaskTex, screenUV).r;
float strength = _Strength * mask * edgeMask;
变体 3:色差扭曲(Chromatic Aberration)
对 RGB 三通道分别用略微不同的偏移量采样,产生色散效果:
hlsl
float2 offset = noise * _Strength * edgeMask;
half r = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, saturate(screenUV + offset * 1.0)).r;
half g = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, saturate(screenUV + offset * 0.95)).g;
half b = SAMPLE_TEXTURE2D(_BlitTexture, sampler_LinearClamp, saturate(screenUV + offset * 0.9 )).b;
return half4(r, g, b, 1.0);
8. 常见问题
Q: 屏幕全黑或画面变成纯色(相机背景色)?
A: 最常见原因是 Shader 直接读取 _BlitTexture 但没有配合 C# Feature 的两步 Blit。必须先用 Blitter.BlitCameraTexture(cmd, source, _tempRT) 把屏幕内容复制到临时 RT,再用 Blitter.BlitCameraTexture(cmd, _tempRT, source, material, 0) 让 Shader 读取它(此时 _tempRT 自动绑定为 _BlitTexture)。仅靠 Full Screen Pass Renderer Feature 内置功能在某些 URP 配置下无法正确捕获颜色缓冲。
Q: ScreenDistortionFeature 在 Add Renderer Feature 列表中找不到?
A: 确认 ScreenDistortionFeature.cs 已保存且 Unity 编译完成(Console 无报错)。脚本文件名必须与类名 ScreenDistortionFeature 完全一致,且不能在任何 namespace 内(或把 Add Renderer Feature 的搜索框清空后滚动查找)。
Q: 扭曲效果存在,但画面边缘出现黑色条带?
A: 偏移后的 UV 超出了 [0,1] 范围,采样到了 RT 边界外。Shader 中已用 saturate(distortedUV) 钳制,如果仍有问题,检查噪声贴图的 Wrap Mode 是否设为 Clamp,或适当减小 _Strength。
Q: 效果在 Scene 视图正常,Game 视图没有?
A: Feature 中已通过 cameraType 判断同时支持 Game 和 SceneView。如果 Game 视图无效,确认 Main Camera 使用的 Renderer Asset 已添加该 Feature,而不是另一个 Renderer Asset。
Q: 运行时动态修改 _Strength 没有效果?
A: 直接通过材质 material.SetFloat("_Strength", value) 修改即可,Feature 每帧执行时会使用材质的当前属性。
Q: 透明物体(Sprite、UI)不受扭曲影响?
A: 这是预期行为。AfterRenderingOpaques 时机只捕获了不透明渲染的结果,透明物体在扭曲 Blit 之后渲染,叠加在扭曲结果上。如果需要扭曲透明物体,改 Injection Point 为 AfterRenderingTransparents,但 UI 仍不受影响(UI 在更晚的时机渲染)。
9. 性能建议
- 噪声图分辨率:128×128 通常已足够,全屏扭曲不需要高频细节。避免使用 512 以上的噪声图,采样开销不值得。
- 临时 RT 的代价 :Feature 会分配一张与相机分辨率相同的临时 RT,有一定内存和带宽开销。
RenderingUtils.ReAllocateIfNeeded会在分辨率不变时复用已有 RT,避免频繁分配。 - 移动端慎用 :全屏两次 Blit 在移动端带宽压力较大,建议降低分辨率(
descriptor.width /= 2等)或限制效果触发频率。 - 关闭时及时 Disable :不需要扭曲效果时,
settings.material = null或整个 Feature Disable,Feature 会提前 return 跳过 EnqueuePass,避免每帧无效 Blit。 - 边缘衰减的开销 :
_EdgeFade的边缘衰减计算极轻,不需要为了性能关掉它,反而能减少边缘撕裂带来的视觉问题。