【Unity Shader URP】屏幕空间扭曲后处理(Screen Space Distortion)实战教程

文章目录

    • [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

  1. 在不透明渲染完成后,把当前颜色缓冲复制到一张临时 RT(_tempRT
  2. 用扭曲 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

  1. Edit → Project Settings → Graphics
  2. 找到 Default Render Pipeline 字段,点击右侧定位图标,Project 窗口高亮对应的 URP Asset
  1. 选中 URP Asset 后,在 Inspector 的 Rendering 分组下,找到 Renderer List,点击其中的 Renderer 进入 URP Renderer Asset

4.2 启用 Opaque Texture(可选)

在 URP Asset → Rendering 下勾选 Opaque Texture 。本效果不强制要求(Shader 读的是 _BlitTexture 而非 _CameraOpaqueTexture),但开启后其他效果也可以使用场景颜色。


5. 使用方法

  1. 创建 Shader 文件 :新建 .shader 文件,粘贴 3.1 节代码,保存为 ScreenDistortion_URP.shader

  2. 创建 C# 脚本 :新建 .cs 文件,粘贴 3.2 节代码,保存为 ScreenDistortionFeature.cs,等待 Unity 编译完成

  3. 创建材质 :新建材质,Shader 选择 Custom/ScreenDistortion_URP,拖入噪声贴图

  4. 添加自定义 Renderer Feature

    • 选中 URP Renderer Asset(New Universal Render Pipeline Asset_Renderer
    • Inspector → Add Renderer Feature → 选择 Screen Distortion Feature
  5. 配置 Feature

    • Material 槽:拖入步骤 3 创建的材质
    • Injection Point:保持 After Rendering Opaques(默认值)
  6. 进入 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 PointAfterRenderingTransparents,但 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 的边缘衰减计算极轻,不需要为了性能关掉它,反而能减少边缘撕裂带来的视觉问题。
相关推荐
晓13131 天前
【Cocos Creator 2.x】篇——第二章 入门
javascript·游戏引擎
音乐宝贝家1 天前
吉他面板材质怎么选?云杉单板面单吉他配置深度解析
数据库·新媒体运营·产品运营·媒体·材质·内容运营
CG_MAGIC1 天前
从光影到物理渲染:Substance Sampler 照片转材质
3d·材质·贴图·uv·建模教程·渲云渲染
nnsix1 天前
Unity 贴图压缩格式 笔记
笔记·unity·贴图
文创工作室1 天前
Adobe Illustrator 中文
ui·adobe·illustrator
ysn111112 天前
搭建状态同步框架的实践心得
unity·架构
烛衔溟2 天前
HarmonyOS 基础 UI 构建 —— 组件、布局与沉浸式效果
ui·华为·harmonyos
一线灵2 天前
Axmol:小众引擎的硬核逆袭
游戏引擎
一次旅行2 天前
CopilotKit实战:用生成式UI打造智能Agent前端
前端·人工智能·ui
weixin_441940012 天前
【Unity教程】使用vuforia创建简单的AR实例
unity·游戏引擎·ar