
兄弟们做 Unity 项目时如果想让画面瞬间从“细腻真实”切换到“复古像素”风格是不是第一反应就是找插件、找商店资源其实像素化Pixelation效果完全不需要付费插件用引擎自带的 RenderTexture、后处理脚本和几行 Shader 就能快速实现。本文会以 Unity VFX视觉特效为切入点完整拆解像素化效果从原理到实战的全流程覆盖传统内置渲染管线和 URP 管线两套方案。无论你是做独立游戏、风格化渲染还是想在技能特效里加入“马赛克闪屏”的视觉冲击都可以直接照做。1. 背景与核心概念1.1 什么是像素化效果像素化效果在游戏和 VFX 里非常常见简单说就是把原本清晰连续的画面强制降低成一个个可见的“方块”。观察者会明显看到锯齿状的色块边界而这种“粗糙感”恰恰能带来复古游戏、赛博故障、抽象艺术、隐私遮挡等不同的视觉暗示。技术层面上像素化不等同于“压缩画质”。它的核心思路是降低画面的采样分辨率再把低分辨率图像拉伸回原始显示尺寸。由于插值算法默认采用双线性或点采样画面中每个像素块内部的颜色趋于统一块与块之间出现明显的跳变就形成了我们熟悉的马赛克感。在 Unity VFX 领域像素化通常用于两类场景全屏风格化整个游戏画面切换到像素风营造复古或抽象氛围。局部特效叠加技能释放瞬间、角色受到控制、场景闪回时给全屏或某个区域叠加像素化扰动制造“故障感”或“异次元传送感”。1.2 核心实现思路对比实现像素化效果常见方案有四种实现方案原理优点缺点适用场景RenderTexture 降采样把相机渲染到低分辨率 RT再全屏拉伸显示性能可控、效果稳定、实现最简单原始画面先低分辨率渲染可能丢失 UI 清晰度全屏像素化后处理 Shader 重采样正常分辨率渲染再在 Shader 里按网格采样屏幕颜色参数灵活、可局部控制需要额外 PassShader 编写有一定门槛局部像素化、动态扰动URP Render Feature在 URP 渲染流程中插入自定义 Pass可定制性最强与现代渲染管线兼容版本迁移成本较高不同 URP 版本 API 有差异URP 项目屏幕空间后处理栈Post Processing V2使用现成的像素化后处理资源接入快、无需改代码版本兼容、性能开销需要实测快速原型本文重点讲解前三种因为它们在 Unity 中更“原生”、不依赖第三方资源也更容易融入 VFX 脚本控制。2. 环境准备与版本说明版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路。2.1 Unity 版本与渲染管线选择本文示例基于以下环境验证Unity 2021.3 LTS 或 Unity 2022.3 LTS内置渲染管线Built-in Render PipelineURP 13.x / 14.x对应 Unity 2021 / 2022Shader 语言ShaderLab / HLSL如果你使用的 Unity 版本更高比如 Unity 6URP 的后处理 API 名称可能有所调整但核心思路不变。代码中我会标注需要按实际版本检查的部分。2.2 项目基础设置新建一个 3D 项目内置管线或 3DURP项目。场景里放一个相机、一个物体比如方块或胶囊体再放一个平行光方便看清画面。如果你选择 URP 方案需要提前通过 Package Manager 安装 Universal Render Pipeline 包并把项目管线和场景管线都切换到 URP 资产。具体路径是Project Settings - Graphics - Scriptable Render Pipeline Settings如果你不想切换项目管线也可以直接在场景相机上挂 URP 相机组件但建议 demo 里直接切换到 URP避免混合管线带来的奇怪问题。3. 像素化核心原理拆解在写代码之前先理解三个核心概念降采样、采样恢复、纹理过滤。3.1 分辨率降采样原理假设相机的原始渲染分辨率为 1920x1080。像素化效果的目标是让画面以更低的逻辑分辨率显示比如 160x90。如果把相机直接渲染到一张 160x90 的 RenderTexture画面信息量就变成了原来的约 1/144。然后再把这张小纹理全屏绘制到屏幕上屏幕上每个显示区域都是同一个 RT 像素被放大的结果。这种做法的好处是相机确实以低分辨率渲染物体计算量降低画面也自然像素化。缺点是UI 如果直接叠加在低分辨率 RT 上UI 也会一起变得马赛克化通常需要把 UI 相机单独分离。如果不想让相机真实降分辨率而是“看起来像降分辨率”就可以用第二种思路在后处理 Shader 里对每个屏幕像素取 UV 网格中心点进行采样。这样画面依然是全分辨率渲染但颜色被强制“块状化”。3.2 后处理材质与 Shader后处理像素化 Shader 的核心代码并不复杂。关键点在于获取屏幕坐标 uv。根据像素化强度 cellSize计算每个块的中心 UV。用中心 UV 对屏幕纹理进行采样。示例 Shader 片段fixed4 frag(v2f i) : SV_Target { float2 uv i.uv; float pixel _CellSize; // 例如 8、16、32 float2 cell floor(uv * _ScreenParams.xy / pixel) * pixel / _ScreenParams.xy; float2 center cell pixel * 0.5 / _ScreenParams.xy; fixed4 col tex2D(_MainTex, center); return col; }这里_ScreenParams.xy是屏幕分辨率pixel表示每个像素块占据的屏幕像素数。数值越大像素块越粗马赛克越明显。3.3 颜色量化色块风格如果读者想进一步做出“像素游戏”的色块感还可以在重采样后对颜色做量化处理也就是把 RGB 通道映射到有限色阶。例如每个通道只保留 4 个色阶col.rgb floor(col.rgb * 4.0) / 4.0;这样做的好处是像素块之间颜色差异更明显。更容易营造复古风格的视觉一致性。和像素化叠加后效果更接近 8-bit 游戏画面。把降采样和颜色量化配合起来才是完整的“像素化 VFX 风格包”。4. 实战方案一内置渲染管线 OnRenderImage如果你还在使用 Unity 内置渲染管线最方便的方式是通过相机上的OnRenderImage回调实现全屏后处理。4.1 创建后处理脚本新建文件夹Scripts创建脚本PixelateEffect.cs。// 文件路径Assets/Scripts/PixelateEffect.cs using UnityEngine; public class PixelateEffect : MonoBehaviour { [Header(像素化强度)] [Range(2, 128)] public int cellSize 16; [Header(是否启用颜色量化)] public bool enableColorQuantization false; [Range(2, 16)] public int colorLevels 4; private Material pixelateMat; private void Awake() { // 创建材质Shader 名称为Hidden/Pixelate/FullScreen pixelateMat new Material(Shader.Find(Hidden/Pixelate/FullScreen)); if (pixelateMat null) { Debug.LogError(未找到像素化 Shader请检查 Shader 文件路径); } } private void OnRenderImage(RenderTexture source, RenderTexture destination) { if (pixelateMat null) { Graphics.Blit(source, destination); return; } pixelateMat.SetFloat(_CellSize, cellSize); pixelateMat.SetFloat(_ColorLevels, colorLevels); if (enableColorQuantization) { pixelateMat.EnableKeyword(COLOR_QUANTIZATION); } else { pixelateMat.DisableKeyword(COLOR_QUANTIZATION); } Graphics.Blit(source, destination, pixelateMat); } private void OnDestroy() { if (pixelateMat ! null) { DestroyImmediate(pixelateMat); } } }脚本说明cellSize是像素块大小数值越大马赛克越粗。enableColorQuantization决定是否启用色阶压缩。OnRenderImage是内置渲染管线的全屏后处理入口URP 中此方法不可用。4.2 编写像素化 Shader创建 Shader 文件放在Assets/Shaders/下。注意 Shader 名称要与 C# 里Shader.Find的名称一致。// 文件路径Assets/Shaders/PixelateFullScreen.shader Shader Hidden/Pixelate/FullScreen { Properties { _MainTex (Texture, 2D) white {} } SubShader { Cull Off ZWrite Off ZTest Always Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ COLOR_QUANTIZATION #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float _CellSize; float _ColorLevels; v2f vert(appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag(v2f i) : SV_Target { float2 uv i.uv; float2 screenSize _ScreenParams.xy; // 计算当前像素所属的像素块 float2 blockIndex floor(uv * screenSize / _CellSize); float2 blockCenter (blockIndex 0.5) * _CellSize / screenSize; // 从最近块中心采样 fixed4 col tex2D(_MainTex, blockCenter); #ifdef COLOR_QUANTIZATION col.rgb floor(col.rgb * _ColorLevels) / _ColorLevels; #endif return col; } ENDCG } } Fallback Off }核心逻辑拆解uv * screenSize / _CellSize把 UV 换算成“块坐标”。floor(...)取整得到当前屏幕像素属于第几个像素块。(blockIndex 0.5) * _CellSize / screenSize计算该块中心 UV。采样中心颜色相当于把整块区域统一成块中心颜色。COLOR_QUANTIZATION关键字控制是否执行色阶压缩。4.3 挂载组件与验证把PixelateEffect.cs挂到主相机上运行场景。你会看到画面出现明显的像素块。调节cellSize数值为 2~8 时是轻度采样感数值为 16~32 时是明显的像素游戏感超过 64 时画面基本只能看到轮廓和色块。预期效果参考cellSize4画面基本正常边缘轻微锯齿。cellSize16模型表面出现马赛克UI 也会被影响。cellSize64画面成为抽象色块VFX 干扰感强烈。由于方案一是全屏后处理UI 也会一起被像素化。如果你希望 UI 保持清晰需要把 UI 放到单独的相机并且保证 UI 相机不参与像素化或者在最终合成阶段把 UI 画面叠加到像素化画面上方。这个做法在项目里很常用建议读者优先掌握相机分离的思路。5. 实战方案二URP Render Feature如果你的项目已经使用 URPOnRenderImage是不可用的。因此需要借助自定义 Renderer Feature 在渲染管线中插入后处理 Pass。注意URP 的 Render Feature API 在不同版本之间有差异。本文示例以 Unity 2021.3 URP 12 或 Unity 2022.3 URP 14 的常见写法为主如果你的版本较新请留意 API 名称变化。5.1 URP 后处理思路URP 渲染流程中自定义后处理本质上是在CommandBuffer中执行一次Blit。我们需要创建一个自定义ScriptableRendererFeature。创建一个自定义ScriptableRenderPass。在Execute方法中获取临时 RT把当前相机纹理 Blit 到临时 RT。将 Shader 材质应用到临时 RT再 Blit 回相机目标。5.2 创建 Renderer Feature创建目录Assets/Scripts/URP/新建 C# 脚本PixelateRenderFeature.cs。// 文件路径Assets/Scripts/URP/PixelateRenderFeature.cs using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; public class PixelateRenderFeature : ScriptableRendererFeature { [System.Serializable] public class Settings { public Material pixelateMaterial; [Range(2, 128)] public int cellSize 16; public bool enableColorQuantization false; [Range(2, 16)] public int colorLevels 4; } public Settings settings new Settings(); private PixelateRenderPass renderPass; public override void Create() { renderPass new PixelateRenderPass(); renderPass.renderPassEvent RenderPassEvent.AfterRenderingPostProcessing; renderPass.Init(settings); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (settings.pixelateMaterial null) { Debug.LogWarning(PixelateRenderFeature: 未指定材质跳过后处理); return; } renderer.EnqueuePass(renderPass); } } public class PixelateRenderPass : ScriptableRenderPass { private Settings settings; private Material material; private RenderTexture tempRT; private ProfilingSampler profilingSampler; public void Init(Settings settings) { this.settings settings; this.material settings.pixelateMaterial; this.profilingSampler new ProfilingSampler(PixelatePass); } public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (material null) return; CommandBuffer cmd CommandBufferPool.Get(PixelatePass); using (new ProfilingScope(cmd, profilingSampler)) { RenderTextureDescriptor descriptor renderingData.cameraData.cameraTargetDescriptor; descriptor.depthBufferBits 0; int tempRTId Shader.PropertyToID(_PixelateTempRT); material.SetFloat(_CellSize, settings.cellSize); material.SetFloat(_ColorLevels, settings.colorLevels); if (settings.enableColorQuantization) material.EnableKeyword(COLOR_QUANTIZATION); else material.DisableKeyword(COLOR_QUANTIZATION); cmd.GetTemporaryRT(tempRTId, descriptor, FilterMode.Point); Blit(cmd, renderingData.cameraData.renderer.cameraColorTargetHandle, tempRTId, material, 0); Blit(cmd, tempRTId, renderingData.cameraData.renderer.cameraColorTargetHandle); } context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); } }需要特别说明的是cameraColorTargetHandle在低版本 URP 中可能是colorAttachment如果编译报错需要根据你的 URP 版本切换 API。GetTemporaryRT的采样模式建议使用FilterMode.Point避免块间边缘插值模糊。Blit 的源目标和临时 RT 声明方式应按照官方 API 调整。本项目里用了 URP 内置的Blit(cmd, source, destination, material, passIndex)扩展方法。5.3 配置 Renderer Feature在 URP 资产中新增 Renderer Feature。具体步骤在项目中找到 URP Asset通常叫URPAsset或UniversalRenderPipelineAsset。选中 URP Asset在 Inspector 中找到Renderer List点击里面的 Forward Renderer。在Forward Renderer资产面板中点击Add Renderer Feature选择PixelateRenderFeature。把像素化材质拖到Pixelate Material字段中。这里使用的材质可以直接使用第 4.2 节创建的内置管线 Shader前提是你把 Shader 放到支持 URP 的 Pass 中。实际上上面的 Shader CGPROGRAM 不依赖光源可以在 URP 下直接复用。只要 Shader 没有依赖内置管线的光照宏Blit 时就能正常显示。运行场景后画面应出现与方案一相同的像素化效果。5.4 URP 版本提示由于 URP 版本迭代较快不同版本的关键 API 名称差异如下Unity 2021.3 URP 12cameraColorTargetHandle可用。Unity 2022.3 URP 14cameraColorTargetHandle可用。更早期 URP 版本可能使用cameraColorTarget。编译报错时优先根据报错提示查阅当前 URP 版本的 ScriptableRenderPass 源码然后再做最小改动。不要盲目复制网上老代码很容易遇到 API 过期问题。6. 实战方案三动态控制与 VFX 联动像素化效果真正出彩的地方在于“动态变化”。如果只是静态马赛克玩法价值有限。下面介绍如何用脚本动态控制像素化强度并实现一个简单的“故障闪屏” VFX。6.1 动态切换分辨率在后处理 Shader 中cellSize直接影响画面像素块大小。动态改变它即可模拟“像素化由弱到强”的效果。扩展PixelateEffect.cs加入协程控制// 文件路径Assets/Scripts/PixelateEffect.cs片段 public IEnumerator FlickerPixelation(float duration, float minCell, float maxCell) { float timer 0; while (timer duration) { cellSize Mathf.RoundToInt(Mathf.Lerp(minCell, maxCell, timer / duration)); timer Time.deltaTime; yield return null; } cellSize Mathf.RoundToInt(minCell); }调用方式StartCoroutine(FlickerPixelation(1.5f, 4f, 48f));这个协程会在 1.5 秒内让像素块从 4 逐渐扩大到 48再瞬间恢复。这种放大-恢复的节奏非常适合技能释放或击杀反馈。6.2 与粒子系统联动更进阶的玩法是当粒子系统触发事件时动态开启像素化并且像素化强度与粒子系统某个参数联动比如time或size。例如在角色释放技能时播放“异空间传送”效果// 文件路径Assets/Scripts/PixelVFXController.cs示意 using System.Collections; using UnityEngine; public class PixelVFXController : MonoBehaviour { public PixelateEffect pixelateEffect; public ParticleSystem portalParticle; public void PlayPortalEffect() { StartCoroutine(PortalSequence()); } private IEnumerator PortalSequence() { if (portalParticle ! null) { portalParticle.Play(); } yield return StartCoroutine(pixelateEffect.FlickerPixelation(0.5f, 4f, 64f)); // 保持最高像素化一段时间 yield return new WaitForSeconds(0.3f); yield return StartCoroutine(pixelateEffect.FlickerPixelation(0.8f, 64f, 4f)); } }这样玩家在释放技能的瞬间获得“画面撕裂成像素块再重组回正常画面”的视觉反馈。实际操作中还可以叠加颜色量化开关先从正常色阶切换到低色阶再切换回来故障感更强烈。6.3 VFX 叠加思路除了像素化本身还可以在像素化基础上叠加其他后处理效果例如颜色偏移RGB 分量错位。亮度闪烁。噪声遮罩。叠加方式很简单在像素化 Shader 中增加一个噪声采样用噪声值混合blockCenter和uv使某些区域像素化较弱、某些区域较强形成局部像素化。这个方向适合有 Shader 基础的读者继续深入本文重点先保证基础像素化效果能落地。7. 常见问题与排查思路像素化效果本身实现不难但很多读者会卡在 Shader 不生效、URP 报错、UI 被模糊等细节上。下面整理几个高频问题。问题现象常见原因解决思路画面全黑或颜色异常Shader 编译错误或_MainTex未正确传入查看 Console 报错确认 Shader 路径检查 Shader 语法没有任何像素化效果OnRenderImage未触发或材质为空确认脚本挂在相机上检查材质是否创建成功内置管线下确认没有启用 Post Processing 栈URP 下 OnRenderImage 不执行URP 不再支持内置后处理入口使用 Renderer Feature 方案UI 也被马赛克化UI 使用了同一个相机分离 UI 相机或把 UI 渲染放到后处理之后像素块边缘模糊插值采样使用了双线性过滤RT 使用FilterMode.PointShader 中采样中心 UV 时不要使用小数偏移cameraColorTargetHandle报错URP 版本 API 不一致查看当前 URP 源码改用对应属性性能明显下降全屏后处理逐像素计算降低屏幕分辨率后处理 Pass控制cellSize不过小禁用多余关键字排查建议按以下顺序执行先看 Console 是否有红色报错。检查 Shader 是否能正常编译Gizmos 面板中查看材质是否有效。在OnRenderImage或Execute里加Debug.Log确认流程是否执行。把cellSize调到最大比如 128确认效果最强时是否出现像素块。如果最大都不出现说明采样逻辑有问题。最后再调整参数范围。8. 最佳实践与工程建议像素化效果写起来很简单但在实际项目中想稳定、高效、可控地使用建议遵循下面几条工程实践。8.1 参数命名与配置隔离像素化的cellSize、色阶数量、过渡时长等参数不建议散落在多个脚本里。建议统一封装到一个PixelateSettings配置脚本中或者使用ScriptableObject做配置资产。这样策划或美术可以无损调整参数不需要改代码。8.2 注意移动端性能全屏后处理在 PC 上开销不大但移动端每帧多一个屏幕分辨率级别的 Pass 会带来额外的带宽压力。移动端建议优先使用 RenderTexture 降采样方案用更小的 RT 直接降低分辨率。需要全屏后处理时尽量把cellSize控制在合理范围。避免在像素化 Shader 中做多次全屏纹理采样。在真机上用 Profiler 测量 GPU 耗时不要只看编辑器性能。8.3 UI 相机分离在很多项目中UI 如果也跟着像素化会让按钮、文字变得难以辨认。建议把 UI 相机独立出来并设置Clear Flags为Depth Only再通过相机层级控制画面叠加顺序。像素化后处理只作用在 3D 相机上UI 保持清晰。8.4 Shader 代码维护Shader 中关键字切换COLOR_QUANTIZATION建议在 C# 脚本里统一管理避免 Shader 内部做大量分支计算。另外不要太依赖Shader.Find项目上线后建议直接引用材质资产避免 Shader 被裁剪导致找不到。Shader.Find在运行时性能较差只推荐用于调试。8.5 安全性提示无论在内置管线还是 URP 中修改后处理都不属于破坏性操作。但如果你在团队项目中频繁改动 Renderer Feature 或 Shader注意先确认当前分支的渲染管线设置避免覆盖队友的处理流程。涉及引擎版本升级时先备份工程再执行渲染管线迁移。9. 总结与扩展方向本文围绕 Unity VFX 中的像素化效果讲解了从原理到落地的完整过程。核心内容包括像素化效果的两种本质实现RenderTexture 降采样和 Shader 重采样。内置渲染管线的OnRenderImage后处理实现。URP 下的 Renderer Feature 实现。通过脚本控制像素化强度实现故障闪屏 VFX。常见问题排查表和移动端性能建议。如果读者已经完成了本文的 demo下一步可以继续探索以下方向增加噪声遮罩制作局部像素化扰动。结合颜色偏移实现赛博故障风格。将像素化效果接入 Timeline 或 Animation Events便于策划编排技能演出。把像素化 Shader 扩展到世界空间对 3D 物体本身进行像素化渲染而不是仅做后处理。像素化效果最吸引人的地方在于它既不依赖昂贵的插件也不需要复杂的渲染框架几行代码就能让画面风格完全改变。动手跑通本文示例之后建议再尝试把cellSize和实际游戏机制关联起来比如“受击时画面像素化”“传送时画面撕裂”等玩法级应用。这种低成本高反馈的实验方式是快速积累 Unity VFX 手感的好路径。快去试试吧。