兄弟们,做 Unity 项目时如果想让画面瞬间从“细腻真实”切换到“复古像素”风格,是不是第一反应就是找插件、找商店资源?其实像素化(Pixelation)效果完全不需要付费插件,用引擎自带的 RenderTexture、后处理脚本和几行 Shader 就能快速实现。
本文会以 Unity VFX(视觉特效)为切入点,完整拆解像素化效果从原理到实战的全流程,覆盖传统内置渲染管线和 URP 管线两套方案。无论你是做独立游戏、风格化渲染,还是想在技能特效里加入“马赛克闪屏”的视觉冲击,都可以直接照做。
1. 背景与核心概念
1.1 什么是像素化效果
像素化效果在游戏和 VFX 里非常常见,简单说就是把原本清晰连续的画面,强制降低成一个个可见的“方块”。观察者会明显看到锯齿状的色块边界,而这种“粗糙感”恰恰能带来复古游戏、赛博故障、抽象艺术、隐私遮挡等不同的视觉暗示。
技术层面上,像素化不等同于“压缩画质”。它的核心思路是降低画面的采样分辨率,再把低分辨率图像拉伸回原始显示尺寸。由于插值算法默认采用双线性或点采样,画面中每个像素块内部的颜色趋于统一,块与块之间出现明显的跳变,就形成了我们熟悉的马赛克感。
在 Unity VFX 领域,像素化通常用于两类场景:
- 全屏风格化:整个游戏画面切换到像素风,营造复古或抽象氛围。
- 局部特效叠加:技能释放瞬间、角色受到控制、场景闪回时,给全屏或某个区域叠加像素化扰动,制造“故障感”或“异次元传送感”。
1.2 核心实现思路对比
实现像素化效果,常见方案有四种:
| 实现方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| RenderTexture 降采样 | 把相机渲染到低分辨率 RT,再全屏拉伸显示 | 性能可控、效果稳定、实现最简单 | 原始画面先低分辨率渲染,可能丢失 UI 清晰度 | 全屏像素化 |
| 后处理 Shader 重采样 | 正常分辨率渲染,再在 Shader 里按网格采样屏幕颜色 | 参数灵活、可局部控制 | 需要额外 Pass,Shader 编写有一定门槛 | 局部像素化、动态扰动 |
| 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 Pipeline)
- URP 13.x / 14.x(对应 Unity 2021 / 2022)
- Shader 语言:ShaderLab / HLSL
如果你使用的 Unity 版本更高(比如 Unity 6),URP 的后处理 API 名称可能有所调整,但核心思路不变。代码中我会标注需要按实际版本检查的部分。
2.2 项目基础设置
新建一个 3D 项目(内置管线)或 3D(URP)项目。场景里放一个相机、一个物体(比如方块或胶囊体),再放一个平行光方便看清画面。
如果你选择 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 时画面基本只能看到轮廓和色块。
预期效果参考:
cellSize=4:画面基本正常,边缘轻微锯齿。cellSize=16:模型表面出现马赛克,UI 也会被影响。cellSize=64:画面成为抽象色块,VFX 干扰感强烈。
由于方案一是全屏后处理,UI 也会一起被像素化。如果你希望 UI 保持清晰,需要把 UI 放到单独的相机,并且保证 UI 相机不参与像素化,或者在最终合成阶段把 UI 画面叠加到像素化画面上方。这个做法在项目里很常用,建议读者优先掌握相机分离的思路。
5. 实战方案二:URP Render Feature
如果你的项目已经使用 URP,OnRenderImage是不可用的。因此需要借助自定义 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 12:
cameraColorTargetHandle可用。 - Unity 2022.3 + URP 14:
cameraColorTargetHandle可用。 - 更早期 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.Point;Shader 中采样中心 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 手感的好路径。快去试试吧。