【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 的边缘衰减计算极轻,不需要为了性能关掉它,反而能减少边缘撕裂带来的视觉问题。
相关推荐
Ulyanov2 小时前
《PySide6 GUI开发指南:QML核心与实践》 第八篇:性能优化大师——QML应用性能调优实战
python·qt·ui·性能优化·qml·系统仿真
报错小能手3 小时前
Swift UI 框架 实战 简易计数器、待办清单 、随机壁纸图库、个人笔记
ui·ios
ai_coder_ai3 小时前
自动化脚本ui编程之flexbox布局
ui·autojs·自动化脚本·冰狐智能辅助·easyclick
zcc8580797623 小时前
Unity 事件驱动架构
unity
心之所向,自强不息3 小时前
VSCode + EmmyLua 调试 Unity Lua(最简接入 + 不阻塞运行版)
vscode·unity·lua
空中海4 小时前
第六篇:Unity专项方向
unity·游戏引擎
3DVisionary4 小时前
混凝土结构力学测试新方案:利用3D DIC技术实现动态裂纹演化三维监测
数码相机·材质·dic技术·新拓三维·混凝土结构·裂纹扩展·材料力学
ZC跨境爬虫4 小时前
UI前端美化技能提升日志day6:(使用苹果字体+计算样式对比差异)
前端·javascript·css·ui·状态模式
mxwin5 小时前
Unity Shader 屏幕空间反射 (SSR) 原理解析
jvm·unity·游戏引擎·shader