资讯动态

Unity像素化效果实现:后处理、URP与对象级Shader三方案详解

发布时间:2026/10/9 18:10:34 来源:尧图企业网站定制
给 Unity 相机画面加像素化效果通常不是为了把画面变模糊而是为了把采样精度压下来让屏幕变成一格一格的低分辨率画面带出强烈的复古感或合成感。这个需求在 VFX 里非常常见——转场、隐私遮挡、像素风角色登场、老电视信号受损都能用同一个内核来实现。这篇文章不是只贴一个 Shader 就结束而是把三条方案都过一遍Built-in 渲染管线下的 OnRenderImage 后处理、URP 的 Renderer Feature 实现、以及只作用于单个对象的 UV 量化 Shader。三条方案各有边界你可以按手头项目的渲染管线选一条直接落地。如果你关心“像素化效果怎么做”“Unity 后处理怎么写”“URP 全屏特效怎么挂”本篇可以直接收藏。涉及到的代码都能复制后跑通但需要你根据 Unity 版本和项目结构做少量调整。1. 像素化效果核心能力速览在开始写代码之前先用一张表把能力边界说清楚。能力项说明适用引擎Unity建议至少 2019.4 LTS 以上URP 方案建议使用 Unity 2021.3 LTS 及以上渲染管线Built-in Render Pipeline / URPSRP效果类型全屏像素化、局部对象像素化、颜色量化、动态像素化实现语言C# ShaderLab / HLSL启动方式引擎内直接挂载脚本或 Renderer Feature无外部服务显存/内存占用取决于屏幕分辨率与临时 RenderTexture 数量需用 Profiler 按项目实测是否支持 API引擎内 C# 接口无 HTTP 外部服务是否支持批量任务可以批量挂到多个相机或物体但不适合当作离线批处理服务硬件要求任意能跑 Unity 的设备均可效果开销中等主要参数像素块大小、颜色量化级别、采样模式、动态强度适合场景复古游戏渲染、过场转场、隐私遮挡、风格化画面从能力表可以看到这个效果的本质是“像素采样”不是“模糊滤镜”。像素化是硬边缘的色块模糊是软边缘的过渡。实现时如果发现画面变糊大概率是采样方式没有对齐到硬像素。2. 像素化效果适用场景与使用边界2.1 适合的典型场景第一类是复古游戏感。把现代 3D 场景整体降到低分辨率再用点采样拉回屏幕尺寸画面会立刻产生“红白机/街机感”。这个做法在独立游戏和 VFX 开场动画里很常见。第二类是隐私遮挡。直播、录屏、取证素材中的人脸、车牌、房间信息用像素化替代马赛克。相比高斯模糊像素化块状更明显也更容易和画面风格统一。第三类是镜头转场。比如章节结束、梦境切入、信号丢失可以把像素块大小从 1 逐渐放大到全屏再反向恢复形成“像素化过场”。2.2 不适合的场景需要保留物体细节时不要用像素化。比如 UI 图标、小字号文字、精细贴图都不适合做全屏像素化否则可读性会大幅下降。另外如果目标是“柔化隐私”高斯模糊通常更合适。2.3 使用边界与合规提醒对包含人脸、车牌、账号、地理位置等敏感信息的画面做遮挡时像素化只是视觉处理手段不必然满足法律层面的“匿名化”要求。真正对外发布前建议咨询负责合规的同事或者再叠加数据脱敏方案。素材方面如果用像素化处理实拍视频、角色模型、商业素材或者用 AI 生成的画面作为输入需要确认原始素材的授权范围和商用许可。像素化不改变素材版权归属。3. 像素化效果环境准备与前置条件在做任何实现前先确认三件事Unity 版本、渲染管线、以及目标设备的 Graphics API。3.1 操作系统与编辑器Windows、macOS、Linux 都可以。关键不是操作系统而是 Unity 编辑器版本。Built-in 方案用的是OnRenderImage在很老的版本里也能用URP 方案依赖ScriptableRendererFeature这是比较新的框架建议用 Unity 2021.3 LTS 或更新版本。3.2 渲染管线选择这是最容易踩坑的地方。同一个项目里要么走 Built-in要么走 URP不建议把两套方案混用。如果项目已经升级到 URP引擎默认不会再执行 MonoBehaviour 上的OnRenderImage。此时需要用 Renderer Feature。如果项目还在 Built-in 管线直接挂脚本即可。3.3 项目目录准备建议提前建立以下目录避免 Shader 和脚本堆在根目录Assets/ ├── Scripts/ │ └── Pixelate/ │ ├── PixelatePostEffect.cs │ ├── PixelateFeature.cs │ └── PixelatePass.cs ├── Shaders/ │ └── Pixelate/ │ ├── HiddenPixelate.shader │ └── PixelateUnlit.shader └── Scenes/ └── PixelateTest.unity3.4 场景准备准备一个带有明显细节的测试场景。最好包含一个带天空盒或纯色背景的相机一个旋转的 3D 物体一个 Canvas上面放几行大号文字。为什么要这样准备因为像素化效果在“高对比边缘”和“文字边缘”最容易被看出问题。没有这些元素你很难判断像素化是否生效以及是否产生波浪纹。4. 方案一Built-in 管线后处理脚本4.1 核心原理在 Built-in 渲染管线中相机执行完所有渲染后会把结果写入source然后经过OnRenderImage输出到destination。我们可以在中间插一步把source缩放到一个很小的 RenderTexture把这个很小的 RT 再放大回destination放大时使用 Point 采样得到硬边缘像素块。这个方案的优势是低分辨率阶段采样次数少移动设备也能接受。相比“单纯对 UV 做 floor”的 Shader 方案真正降低了 GPU 带宽。4.2 贴到相机上的 C# 脚本在Assets/Scripts/Pixelate/PixelatePostEffect.cs中写入如下代码using UnityEngine; public class PixelatePostEffect : MonoBehaviour { [Header(像素块大小8 每 8x8 屏幕像素合并为一块)] public int pixelBlock 8; [Header(true 使用硬边缘点采样false 会有轻微软边)] public bool usePointSampling true; private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (pixelBlock 1) pixelBlock 1; // 计算低分辨率尺寸避免被除成 0 或负数 int lowWidth Mathf.Max(2, source.width / pixelBlock); int lowHeight Mathf.Max(2, source.height / pixelBlock); RenderTextureDescriptor desc source.descriptor; desc.width lowWidth; desc.height lowHeight; desc.depthBufferBits 0; RenderTexture lowRes RenderTexture.GetTemporary(desc); lowRes.filterMode usePointSampling ? FilterMode.Point : FilterMode.Bilinear; // 第一步把原始图像压到低分辨率 RT Graphics.Blit(source, lowRes); // 第二步把低分辨率 RT 放大回屏幕点采样产生像素块 Graphics.Blit(lowRes, destination); RenderTexture.ReleaseTemporary(lowRes); } }这段代码的核心只有两次Graphics.Blit。第一次缩小第二次放大。关键在于lowRes.filterMode必须设置为FilterMode.Point否则低分辨率画面被拉回屏幕时会插值像素块边缘会发虚。4.3 挂载与运行步骤在 Hierarchy 中找到 Main Camera为相机添加组件PixelatePostEffect运行场景。运行后相机的输出会立刻变成马赛克风格。pixelBlock越大画面颗粒越粗。从 4 调到 16可以明显看到色块数量减少。4.4 怎么确认效果是“成功”的判断标准有三个画面中被分割成方格色块边缘清晰物体轮廓仍然可辨认只是采样精度降低运行时不报NullReferenceException或MissingReferenceException。如果画面变模糊而不是成块说明低分辨率 RT 的过滤模式不对。确认lowRes.filterMode FilterMode.Point已执行。5. 方案二URP Renderer Feature 全屏像素化5.1 为什么 URP 不能直接用 OnRenderImageURP 使用可编程渲染管线相机输出透明的规则和 Built-in 不同。旧式的OnRenderImage调用在 URP 中默认不会执行必须把后处理逻辑封装成ScriptableRendererFeature并添加到 URP 的 Renderer Data 中。这套流程的好处是你可以在 URP 资产里灵活开关不同后处理和通用渲染管线自带的 Bloom、Color Adjustments 并列管理。5.2 Feature 与 Pass 分离创建Assets/Scripts/Pixelate/PixelateFeature.csusing UnityEngine; using UnityEngine.Rendering.Universal; public class PixelateFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public Material pixelateMaterial; public int pixelBlock 8; public RenderPassEvent renderPassEvent RenderPassEvent.BeforeRenderingPostProcessing; } public Settings settings new Settings(); private PixelatePass pixelatePass; public override void Create() { pixelatePass new PixelatePass(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.pixelateMaterial null) return; renderer.EnqueuePass(pixelatePass); } }再创建Assets/Scripts/Pixelate/PixelatePass.csusing UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class PixelatePass : ScriptableRenderPass { private readonly PixelateFeature.Settings settings; private readonly int lowResRTID Shader.PropertyToID(_PixelateLowResRT); private RenderTextureDescriptor lowResDescriptor; public PixelatePass(PixelateFeature.Settings settings) { this.settings settings; this.renderPassEvent settings.renderPassEvent; } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { RenderTextureDescriptor desc renderingData.cameraData.cameraTargetDescriptor; desc.width Mathf.Max(2, desc.width / settings.pixelBlock); desc.height Mathf.Max(2, desc.height / settings.pixelBlock); desc.depthBufferBits 0; lowResDescriptor desc; } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { CommandBuffer cmd CommandBufferPool.Get(PixelatePass); var source renderingData.cameraData.renderer.cameraColorTarget; cmd.GetTemporaryRT(lowResRTID, lowResDescriptor, FilterMode.Point); Blit(cmd, source, lowResRTID); Blit(cmd, lowResRTID, source); context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } public override void OnCameraCleanup(CommandBuffer cmd) { cmd.ReleaseTemporaryRT(lowResRTID); } }不同 URP 版本中获取相机颜色目标的方式可能略有差异。renderingData.cameraData.renderer.cameraColorTarget在多数 Unity 2021.3 以上版本可用但如果你使用更早的 URP 包可能需要从RenderTextureHandle或其他字段拿。遇到编译错误时优先检查当前 Unity 版本对应的 URP API 文档。5.3 在 URP Asset 中添加 Renderer Feature打开 Project Settings 中使用的 URP Renderer Data 资产在 Inspector 的 Renderer Features 列表点击 Add Renderer Feature选择Pixelate Feature把pixelateMaterial指向一个能放大低分辨率 RT 的 URP Shader 材质。如果没有设置 MaterialFeature 会在AddRenderPasses阶段直接 return画面不变。这是很多人添加后毫无反应的原因。5.4 URP 放大 Shader创建Assets/Shaders/Pixelate/HiddenPixelateURP.shaderShader Hidden/Pixelate/URPBlit { SubShader { Tags { RenderType Opaque RenderPipeline UniversalPipeline } Pass { ZWrite Off Cull Off HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); float4 _MainTex_TexelSize; struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; }; Varyings vert(Attributes input) { Varyings output; output.positionCS TransformObjectToHClip(input.positionOS.xyz); output.uv input.uv; return output; } half4 frag(Varyings input) : SV_Target { float2 pixelUv float2( floor(input.uv.x * _MainTex_TexelSize.z) / _MainTex_TexelSize.z, floor(input.uv.y * _MainTex_TexelSize.w) / _MainTex_TexelSize.w ); return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, pixelUv); } ENDHLSL } } }这个 Shader 的关键是_MainTex_TexelSize.z / _MainTex_TexelSize.w。它们表示低分辨率 RT 的宽和高。把 UV 对齐到低分辨率 RT 的像素中心再做一次采样就可以强制呈现“硬像素块”效果。6. 方案三对象级 UV 量化 Shader6.1 什么时候用对象级方案后处理方案影响整个相机画面。但很多 VFX 只需要局部像素化比如某个平面上的传送门、某个角色模型块状化、或者一个沿地面扩散的像素化圆。此时用对象级 Shader 更合适成本更低而且能配合顶点动画做出更多变化。在Assets/Shaders/Pixelate/PixelateUnlit.shader中写入如下代码Shader Custom/PixelateUnlit { Properties { _MainTex (Texture, 2D) white {} _GridSize (Grid Size, Float) 16 _Brightness (Brightness, Range(0, 2)) 1 _Alpha (Alpha, Range(0, 1)) 1 } SubShader { Tags { Queue Transparent RenderType Transparent } Blend SrcAlpha OneMinusSrcAlpha Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; float _GridSize; float _Brightness; float _Alpha; v2f vert(appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag(v2f i) : SV_Target { float2 block floor(i.uv * _GridSize) / _GridSize; fixed4 col tex2D(_MainTex, block); col.rgb * _Brightness; col.a * _Alpha; return col; } ENDCG } } }把这个材质赋值给一个 Plane 或 Quad调整_GridSize就能看到物体表面的纹理被切成方块。需要动态效果时可以在脚本里每帧改变_GridSizeusing UnityEngine; public class PixelateObjectAnimator : MonoBehaviour { public Renderer targetRenderer; public float minGrid 2f; public float maxGrid 32f; public float speed 2f; private MaterialPropertyBlock block; private float time; private void Start() { block new MaterialPropertyBlock(); } private void Update() { time Time.deltaTime * speed; float grid Mathf.Lerp(minGrid, maxGrid, (Mathf.Sin(time) 1f) * 0.5f); targetRenderer.GetPropertyBlock(block); block.SetFloat(_GridSize, grid); targetRenderer.SetPropertyBlock(block); } }使用MaterialPropertyBlock可以避免为每个物体创建独立材质实例。同一个材质下多个物体如果使用同一个属性名会得到不同参数而不会互相污染。6.2 拓展出来的一层像素风 VFX把像素化 Shader 和粒子系统结合可以得到类似“像素化粒子消散”的效果。常见做法是把粒子材质换成PixelateUnlit材质让粒子自身的 UV 被打乱成块状再配合粒子系统的旋转、大小和颜色变化形成像素颗粒从物体上剥离的感觉。如果做老电视故障效果还可以叠加两个变量一个是_GridSize随时间抖动另一个是颜色量化// 在 frag 中追加颜色量化 float quantize 8.0; col.rgb floor(col.rgb * quantize) / quantize;颜色量化把 RGB 从 256 级压到更少等级画面会出现更强烈的“色彩断层”非常适合故障效果。7. 像素化效果性能观察与资源占用7.1 怎么看性能Unity 里看性能最直接的工具是 Profiler 窗口。打开 Window Analysis Profiler切到 Rendering 模式再运行场景关注 Game 视图的实际帧耗时。在低端测试机上重点看 GPU 时间而不是 Editor 的帧率因为编辑器里调用 Shader 编译和 Gizmos 会干扰结果。7.2 三个方案的开销差异方案一Built-in 低分辨率 RT 方案需要一次临时 RT 分配两次Graphics.Blit。优点是把后续采样面积缩小到1 / (pixelBlock * pixelBlock)。对于全屏 VFX 来说这是最高性价比的做法。方案二URP Feature 方案开销类似方案一但多了Renderer Feature的调度开销。在低端设备上建议把 Shader 内部的对齐计算放到 Fragment 阶段一次完成不要做多次采样。方案三对象级 UV 量化方案采样面积没有降低它仍然是全屏对象分辨率只是 UV 坐标被改写了。性能提升并不明显但是局部使用成本非常低。7.3 临时 RT 泄漏是最大隐患在 Built-in 方案中如果忘掉RenderTexture.ReleaseTemporary每帧都会占用额外显存。最直接的检查方法是连续运行场景打开 Profiler 的 Memory 模块观察 RenderTexture 数量是否每帧上涨。如果上涨说明临时 RT 没有释放。在 URP 方案中OnCameraCleanup里释放lowResRTID是必须的。不同相机切换时未释放的 RT 会导致显存峰值快速上涨。7.4 移动端和 WebGL 注意事项移动端渲染格式建议使用RenderTextureFormat.RGB565或根据平台调整能减少带宽。但颜色精度要求高时不要强压否则会出现明显色带。WebGL 上做全屏后处理需要确认是否启用 MSAA。像素化效果本身不需要高精度如果同时开 MSAA 和高分辨率 RT反而浪费带宽。实际项目里可以先在 Profiler 里开 WebGL 预览跑一遍再决定是否降低分辨率。7.5 关于接口 API 的说明这个效果是引擎内 VFX 组件不提供 HTTP API也没有离线批量任务入口。如果你需要把一批视频帧处理成像素化画面建议在 Unity 中录制相机画面后再用 Shader 后处理输出而不是试图把像素化算法拆成外部服务。8. 像素化效果常见问题与排查方法问题现象可能原因排查方式解决方案后处理没有生效画面正常相机上没挂脚本或 URP 没添加 Renderer Feature检查 Hierarchy 中的相机组件检查 URP Renderer Data 的 Renderer Features 列表挂上脚本或在 URP 资产中添加 Feature画面变成模糊而不是像素块低分辨率 RT 过滤模式不是 Point检查lowRes.filterMode或检查 Shader 是否做了 UV 对齐设置为FilterMode.Point并确认 Shader 中没有额外插值像素块大小与屏幕分辨率不匹配pixelBlock是绝对像素值不同分辨率下看起来大小不一致在 Editor 切换 Game 视图分辨率观察变化改为按相对比例计算比如screenWidth / desiredBlocks运行时报错RenderTexture format is not supported平台不支持源 RT 的默认格式换用source.descriptor创建临时 RT或指定明确格式减少 depth 位或用线性空间下常用的 RGBA 格式确认已经添加脚本但 URP 运行时黑屏Feature 中 Material 为空或 Pass 中 Blit 目标错误查看 URP 的 Renderer Feature 面板打开 Frame Debugger为 Material 赋值并检查cameraColorTarget获取方式在 Built-in 管线运行正常切到 URP 后失效OnRenderImage在 URP 中不再被调用确认项目是否已切换 URP改用ScriptableRendererFeature方案Memory 中 RenderTexture 数量持续上涨临时 RT 没有释放在 Profiler Memory 模块搜索 RT 名补充ReleaseTemporaryRT或cmd.ReleaseTemporaryRT文字边缘出现严重锯齿像素化本质是低精度采样小字号文字信息丢失把文字区域排除或用更大的字号通过蒙版或区域遮罩避开 UI 文字区WebGL 运行明显卡顿全屏 RT 过大或采样次数过多在 Profiler 中看 GPU 时间提高pixelBlock降低低分辨率 RT 尺寸排查顺序建议先确认渲染管线再确认 Shader 是否编译通过最后看临时 RT 是否被释放。大多数“画面没变化”或“黑屏”的问题都出在这三处。9. 像素化效果最佳实践与使用建议9.1 第一次测试时缩小参数范围不要一上来就设置pixelBlock 64。先分别测试 4、8、16、32 四档观察画面边缘和文字区域的变化。像素块越大原始画面信息越少对场景构图影响也越大。保留一组最小可运行参数后续再一点点加大。9.2 脚本和 Shader 都要做资源保护在 C# 脚本中Shader.Find(Hidden/Pixelate/URPBlit)不应该每帧执行。可以在OnEnable或Awake中缓存 Materialprivate Material cachedMaterial; private void OnEnable() { Shader shader Shader.Find(Hidden/Pixelate/URPBlit); cachedMaterial new Material(shader); }如果找不到 Shader启动时打印一次 Warning而不是每帧报错。9.3 用临时 RT 时优先描述符尽量使用source.descriptor或cameraTargetDescriptor派生临时 RT而不是写死宽高。这样分辨率变化、HDR 开关、MSAA 状态变化时RT 格式仍然匹配。9.4 区域遮罩与外部接入如果想把像素化限制在画面某个圆内可以在 Shader 中传入一个中心点和半径对 UV 做距离判断只对半径内部执行像素化。这个思路和喷绘遮罩、隐私遮挡框类似。实际接入时把遮罩参数放在MaterialPropertyBlock或一个共享参数组件中即可。9.5 版权与素材合规像素化效果适合做风格化处理和演示测试但不要假设它能把所有内容“自动脱敏”。如果要对外发布包含人物肖像、品牌 Logo 或第三方素材的画面先完成授权核对。对合成素材、录屏数据也要明确数据来源和合规边界。10. 总结与下一步这个项目最值得尝试的地方在于实现路径非常短三条方案加起来也不到几百行代码却能覆盖全屏后处理和局部 VFX 两种场景。建议先跑通 Built-in 方案把“低分辨率 RT Point 采样”的核心逻辑记下来再迁移到 URP 的 Renderer Feature。最先需要验证的是调整pixelBlock时画面是否出现气块状过渡以及镜头移动时是否出现明显闪烁。最容易踩的坑是 URP 版本 API 差异和临时 RT 泄漏。前者依赖你阅读当前 URP 包的文档后者靠ReleaseTemporaryRT就能解决。后续可以继续扩展的方向包括颜色量化、边缘保留、故障雪花效果、像素化转场、配合 Timeline 做帧动画以及把像素化参数暴露给 UI 面板让美术同学在运行时可调。像素化效果最大的价值是让开发者用最简单的方式把画面“拉出时代感”。你不需要引入第三方插件也不用改造渲染管线把一个 Shader 和一段脚本管理好就已经能覆盖大多数 VFX 需求。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑