多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Unity像素化实现:RenderTexture、后处理Shader与粒子方案

Unity像素化实现:RenderTexture、后处理Shader与粒子方案 做像素风游戏或者给场景里加一段复古特效时很多人第一反应是给相机挂一个模糊Shader或者用后处理插件去“模拟”低分辨率。结果做出来的画面往往是模糊的、脏的和真正的像素感差了很远。像素化效果在 Unity 里实现起来并不复杂核心思路也不是加滤镜而是降低采样频率。只要把相机渲染到一张低分辨率的 RenderTexture 上再用 Point 过滤模式放大显示就能得到干净利落的像素块。但如果只用默认方式你大概率会得到一张被 GPU 平滑插值过的模糊图而不是想要的像素风。这篇文章会从原理讲起给出三种在实际项目中可以落地的做法RenderTexture 方案、后处理 Shader 方案以及粒子系统和 VFX Graph 里的像素风格处理。每种方案都会给出可复制的完整代码、运行步骤和验证方法。文末还整理了最容易踩的坑和工程实践建议方便你直接迁移到自己的项目里。1. 这篇文章真正要解决的问题我一直觉得“像素化”是 Unity 开发里被严重低估的一个小需求。很多人以为它只是把画质调低于是直接把相机的分辨率改成 160×90然后发现画面糊成一团也有人用了后处理 Shader但像素块大小不均匀边缘还有奇怪的紫边还有人在特效里单独设置了 Pixel Art 材质但镜头一拉远粒子和背景的像素密度就对不上。这些问题的根源是没有搞懂像素化的两个关键参数实际渲染分辨率和像素块的采样方式。这篇文章要解决的就是三个最实际的问题如何用 RenderTexture 最短路径实现全局像素化包括 UI 显示和相机输出。如何用后处理 Shader 控制像素块大小不依赖低分辨率 RT适合局部特效或动态切换。如何在粒子系统和 VFX Graph 中保持像素风格避免粒子和背景脱节。读完之后你可以根据项目阶段选择方案原型快速验证用方案一产品化控制画面风格用方案二处理特效细节用方案三。三种方案可以叠加使用这也是很多像素风商业项目的实际做法。2. 像素化的核心原理为什么会模糊像素化效果在数学上很简单就是把一张图像按照固定格子重新采样每个格子内部的颜色取一个代表值。举例来说一张 1920×1080 的画面如果每个像素块是 16×16那么画面就变成了 120×68 个色块的排列。所以实现像素化有两条路降低渲染目标的分辨率让 GPU 把颜色计算到一张很小的纹理上。在不对原始分辨率做改变的情况下把 UV 坐标按格子取整让每个区域只采样一次纹理颜色。两条路最终都指向同一个目标让每个像素块内部的颜色一致、边界清晰。真正让它不模糊的是Point 过滤模式。这里必须强调一个新手几乎必踩的坑GPU 在把小纹理放大到屏幕时默认会做 Bilinear 插值也就是在两个相邻像素之间计算出一个过渡色。这个过渡色会让像素块边缘产生渐变所以看起来是模糊的。只有把纹理的filterMode设置为FilterMode.PointGPU 才会直接取最近的像素颜色边缘才会是锐利的方块。理解了这一点后面对 RenderTexture、Shader、粒子的所有配置就都有依据了。3. 环境准备与前置条件本文的代码基于 Unity 2021.3 LTS 或更高版本测试但基本原理同样适用于 Unity 2019 和 2022。如果你用的是 Unity 6代码也兼容。需要注意URP 和内置渲染管线的后处理写法略有不同本文主要以内置管线为准最后会补充 URP 下的调整建议。你需要准备以下环境项目要求操作系统Windows / macOS / Linux 均可Unity 版本Unity 2021.3 LTS 以上内置渲染管线基础内容一个空的 3D 或 URP 场景一个相机组件知识了解 Camera、RawImage、Material 的基本用法为了演示粒子相关部分建议在场景里新建一个默认的 Particle System并导入一张带 Alpha 的粒子贴图。如果你用 URP后处理部分建议改为使用 Volume 或 Renderer Feature但核心 Shader 的原理不变。4. 方案一RenderTexture 低分辨率方案这个方案是“最短路径”适合原型验证、回合制游戏角色对话、Boss 出场动画等全局整屏像素化的场景。4.1 创建 RenderTexture 并设置过滤模式在 Unity 中RenderTexture 可以手动在 Assets 面板创建也可以用代码创建。我更推荐用代码创建这样可以动态调整分辨率也方便在运行时修改像素强度。// 文件路径Assets/Scripts/PixelationRenderer.cs using UnityEngine; using UnityEngine.UI; public class PixelationRenderer : MonoBehaviour { [Header(像素分辨率)] [SerializeField] private int pixelWidth 160; [SerializeField] private int pixelHeight 90; [Header(组件引用)] [SerializeField] private Camera targetCamera; [SerializeField] private RawImage displayImage; private RenderTexture _renderTexture; private void Awake() { if (targetCamera null) { targetCamera Camera.main; } // 创建低分辨率 RenderTexture _renderTexture new RenderTexture(pixelWidth, pixelHeight, 24, RenderTextureFormat.ARGB32); // 关键Point 过滤保留像素块 _renderTexture.filterMode FilterMode.Point; _renderTexture.wrapMode TextureWrapMode.Clamp; _renderTexture.Create(); // 相机渲染到 RenderTexture targetCamera.targetTexture _renderTexture; // 在 UI 上显示 if (displayImage ! null) { displayImage.texture _renderTexture; } } private void OnDestroy() { if (targetCamera ! null) { targetCamera.targetTexture null; } if (_renderTexture ! null) { _renderTexture.Release(); Destroy(_renderTexture); } } }这段代码的关键有三个FilterMode.Point、相机targetTexture、RawImage 的texture。FilterMode.Point保证了小纹理放大时不会插值相机输出到这个 RT 后画面色彩和光影都在 160×90 的尺寸内完成计算RawImage 再把这张 RT 显示到 UI 上。4.2 UI 上的 RawImage 要单独配置材质吗不需要。只要 RT 的过滤模式是 PointRawImage 默认采样方式就会保留像素感。不过你需要在 Canvas 上给 RawImage 设置一个合适的 RectTransform 尺寸拉开比例才能看到像素块。如果你的 RawImage 显示出来依然模糊优先排查 Canvas 的Filter Mode设置和导入设置。这里有个容易忽略的点RawImage 在 UI 中显示纹理时受 Canvas 缩放和 Shader 采样影响如果 Canvas 的 Scale Factor 为小数也会在视觉上产生模糊。尽量让 RawImage 的像素尺寸与屏幕分辨率成整数倍关系。4.3 运行后如何恢复相机渲染上面的脚本在OnDestroy中把targetCamera.targetTexture恢复成了 null这是必要的清理步骤。否则场景切换后相机继续渲染到旧 RT可能出现黑屏或内存泄漏。这个方案的完整流程是渲染 → 低分辨率 RT → Point 放大 → UI 显示。它适合整屏像素化缺点是 UI 仍然是原始的不会一起像素化。如果你希望 UI 也像素化可以把整个 Screen Space Camera Canvas 也渲染进相机或者使用后处理方案。5. 方案二后处理 Shader 方案当你想在完整分辨率下控制像素块大小或者做局部区域像素化、动态切换像素密度时后处理 Shader 更合适。这个方案不改变渲染分辨率而是在最终输出阶段重新采样。5.1 像素化后处理 Shader在 Assets 下创建Shaders/Pixelation.shader内容如下// 文件路径Assets/Shaders/Pixelation.shader Shader Custom/Pixelation { Properties { _MainTex (Base (RGB), 2D) white {} _PixelSize (Pixel Size, Range(1, 128)) 8 } SubShader { Tags { RenderTypeOpaque } Cull Off ZWrite Off ZTest Always 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_TexelSize; float _PixelSize; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } fixed4 frag (v2f i) : SV_Target { float2 texel _MainTex_TexelSize.xy; // 计算一个像素块在 UV 空间中的大小 float2 blockSize texel * _PixelSize; // 按格子对齐 UV float2 uv floor(i.uv / blockSize) * blockSize; // 防止边缘越界 uv saturate(uv); return tex2D(_MainTex, uv); } ENDCG } } Fallback Off }理解这个 Shader 的关键点_MainTex_TexelSize.xy是主纹理单个像素的 UV 大小例如 1920×1080 下约为(1/1920, 1/1080)。texel * _PixelSize得到的是“像素块大小”在 UV 空间的跨度。floor把 UV 对齐到最近的像素块左下角。saturate防止越界采样导致边缘颜色异常。你可以把_PixelSize理解为“每个像素块包含多少个原始像素”。数值越大像素块越大画面越粗。5.2 后处理脚本// 文件路径Assets/Scripts/PixelationEffect.cs using UnityEngine; [ExecuteInEditMode] [RequireComponent(typeof(Camera))] public class PixelationEffect : MonoBehaviour { [SerializeField] private Shader pixelationShader; [SerializeField, Range(1f, 128f)] private float pixelSize 8f; private Material _pixelationMaterial; private void Awake() { if (pixelationShader null) { pixelationShader Shader.Find(Custom/Pixelation); } _pixelationMaterial new Material(pixelationShader); _pixelationMaterial.hideFlags HideFlags.HideAndDontSave; } private void OnRenderImage(RenderTexture src, RenderTexture dst) { if (_pixelationMaterial null || pixelSize 0f) { Graphics.Blit(src, dst); return; } _pixelationMaterial.SetFloat(_PixelSize, pixelSize); Graphics.Blit(src, dst, _pixelationMaterial); } private void OnDestroy() { if (_pixelationMaterial ! null) { DestroyImmediate(_pixelationMaterial); } } }将这个脚本挂到主相机上pixelationShader会自动查找Custom/Pixelation。运行时滑动物体的pixelSize画面就会实时变粗或变细。Graphics.Blit会把OnRenderImage的输入图像 src 交给材质处理后输出到 dst。如果材质为 null就直接复制源图像保证误配置时不会黑屏。5.3 URP 下的变通做法在 URP 项目中OnRenderImage默认不会触发。你需要使用 Renderer Feature 或者ScriptableRenderPass。最省力的做法是在 URP 的 Volume 中挂一个自定义的FullScreenPassRendererFeature或者用Blit方法。如果你在用 Unity 2022也可以使用Full Screen Pass Renderer Feature来注入这个 Shader。从实际项目看我认为后处理方案是最终要用的方案它不牺牲渲染精度可以控制局部特效角度像素块大小还能平滑动画。缺点是每帧多一次全屏采样性能开销略高于方案一。6. 方案三粒子系统和 VFX Graph 的像素化处理很多人做像素风 VFX 会卡在一个问题上背景已经像素化了但粒子效果还是丝滑的感觉像两个图层叠加在一起非常出戏。这个问题的本质是粒子的纹理过滤方式和背景不一致。Unity 粒子系统的 Sprite 渲染默认使用纹理原本的过滤设置如果纹理是 Bilinear粒子就会显得平滑而背景是 Point 放大观感自然割裂。6.1 粒子贴图改为 Point 过滤最简单的方法是在导入器里处理在 Project 窗口选中粒子贴图。Inspector 中把Filter Mode改成Point (no filter)。把Compression设为None或High Quality避免压缩导致像素块变形。点击Apply。这是一个非常容易忽略的坑很多像素风粒子贴图导入时被默认设置为了 Bilinear即使你在运行时把材质的过滤模式改了导入器的设置也会覆盖。6.2 代码控制粒子材质如果粒子系统是由代码动态创建的或者你想在运行时切换多种材质可以写一个辅助脚本// 文件路径Assets/Scripts/PixelParticleHelper.cs using UnityEngine; public class PixelParticleHelper : MonoBehaviour { [SerializeField] private ParticleSystem particleSystem; [Header(纹理点过滤)] [SerializeField] private bool enablePointFilter true; [Header(粒子纹理随机旋转增强像素感)] [SerializeField] private bool randomRotation false; private void Awake() { if (particleSystem null) { particleSystem GetComponentParticleSystem(); } if (particleSystem null) { Debug.LogWarning(PixelParticleHelper: ParticleSystem 未找到); return; } // 对粒子渲染器使用的所有材质设置点过滤 var renderer particleSystem.GetComponentRenderer(); if (renderer ! null) { Material[] materials renderer.materials; for (int i 0; i materials.Length; i) { if (materials[i] ! null) { materials[i].mainTexture.filterMode FilterMode.Point; } } renderer.materials materials; } if (randomRotation) { var main particleSystem.main; main.startRotation new ParticleSystem.MinMaxCurve(0f, Mathf.PI); } } }这段代码会在运行时把所有粒子材质的纹理过滤设置为FilterMode.Point。mainTexture.filterMode是运行时最直接的修改点比逐个找材质再设置贴图更可靠。6.3 VFX Graph 的像素化注意点Visual Effect Graph 的纹理采样默认和 Unity 纹理设置一致。如果你用默认粒子纹理它可能是 Bilinear 采样所以视觉上和像素风背景不匹配。在 VFX Graph 中做像素化常见做法是使用Sample Texture2D节点时把采样器的过滤模式改为Point。不同 Unity 版本界面差异较大但大体都有FilterMode选项。如果版本里没有暴露 FilterMode可以在导入 VFX 所用纹理时设置为 Point这会影响默认采样。使用Skinned Mesh Renderer 粒子输出时酌情在材质属性里的纹理过滤改为 Nearest。从工程角度看更省心的做法是把全局像素化后处理应用到整个相机这样 VFX、粒子、背景都会被一起像素化风格统一。如果你只想让粒子单独像素化而背景不像素化就按上面的方式单独处理粒子的纹理过滤和分辨率。7. 运行结果与效果验证三种方案的验证方式略有不同。7.1 方案一验证运行场景观察 RawImage 上是否出现清晰的像素块。如果出现模糊而不是像素块按以下顺序排查检查 RT 的filterMode是否确实是 Point。在 Inspector 或 Debug 窗口查看。检查 RawImage 对应的 Canvas Render Mode。如果是 Screen Space - Overlay确认 RawImage 尺寸与屏幕比例合理。检查相机的 Clear Flags 和 Culling Mask确保场景物体确实被渲染到了 RT。这个方案的另一个显著特征是相机不再直接输出到 Game 窗口而是输出到 RawImage。如果你发现 Game 窗口变成黑屏或空白不要慌这是预期行为因为你把输出目标从屏幕切到了 RT。7.2 方案二验证运行场景后你会看到整个屏幕实时发生变化pixelSize为 1 时几乎看不出差异为 8 时出现明显像素块为 32 时画面变成一个一个大的色块。如果 Shader 没有生效最可能的原因是 Shader 编译失败。在 Console 窗口查看有没有Shader error日志。如果有优先检查 Shader 里的CGPROGRAM语法注意fixed4和float2是不是写成了中文符号。7.3 粒子验证在场景中同时放置一个默认粒子系统和一个经过PixelParticleHelper处理的粒子系统对比两者与背景像素化的契合度。处理过的粒子边缘应该锐利和背景的像素块大小协调。如果粒子太小可能看不出差别。建议把粒子尺寸放大到屏幕 1/10 左右再观察边缘。8. 常见问题与排查思路问题现象可能原因排查方式解决方案RawImage 显示模糊而非像素块RT 过滤模式为 Bilinear查看 RT 的 filterMode改为 FilterMode.PointGame 窗口黑屏相机渲染到了 RT没有输出到屏幕检查相机的 targetTexture在 OnDestroy 中恢复 targetTexture 为 null后处理 Shader 不生效使用了 URPOnRenderImage 不触发查看渲染管线使用 Full Screen Pass Renderer Feature像素块大小不均匀Shader 中 UV 越界或 blockSize 是小数打断点查看 uv 值对 uv 使用 saturate 或 clamp粒子边缘模糊粒子贴图过滤模式为 Bilinear查看贴图导入设置改为 Point (no filter)像素化后颜色发灰RT 深度格式不正确检查 RT depth使用 16 或 24 位深度缓冲切换场景后 RT 泄露没有释放 RenderTexture查看内存 ProfilerOnDestroy 中 Release 并 Destroy有一个容易误解的点是_PixelSize越大画面并不是变得更“模糊”而是变得更“粗糙”。模糊是线性插值的结果粗糙是离散采样的结果。像素化追求的是后者这也是为什么我们反复强调 Point 过滤。9. 最佳实践与工程建议像素化效果虽然代码量不大但放到实际项目中仍然有几个值得讲究的地方。9.1 使用 Shader Property 控制强度而不是直接改分辨率方案二的后处理脚本里已经用_PixelSize控制了像素块大小。这个值可以在游戏里做成一个全局变量用于 Boss 出场动画、剧情演出、Skill 特效的转场。这种方式比直接修改相机分辨率更平滑也不会引起 RT 重新创建的开销。9.2 注意移动端性能后处理方案在移动端每帧做一次全屏 Blit相当于一次全屏绘制调用。低端安卓机上如果_PixelSize很小开销会更明显。移动端项目建议优先考虑方案一因为 160×90 的 RT 渲染压力远小于 1080p 后处理。9.3 避免在 RenderTexture 上使用 Tile 模式当 RT 用作像素化输出时wrapMode必须设置成Clamp。如果设置成Repeat边缘的 UV 采样会取到对侧像素导致画面边缘出现明显的色带跳变。9.4 把像素化效果和相机裁剪分离如果你只想让部分画面像素化例如只像素化场景不像素化血条可以准备两个相机一个普通相机渲染 UI另一个像素化相机渲染世界然后用 Renderer Feature 或命令缓冲合并。9.5 版本控制与团队协作Shader 和渲染管线相关代码在不同 Unity 版本间差异较大。建议在项目说明里写明 Unity 版本和渲染管线。如果项目从内置管线升级到 URP后处理代码需要重写而 RenderTexture 方案的代码基本不变。10. 总结与后续学习方向这个效果看起来简单实际上包含了纹理采样、过滤模式、后处理生命周期、相机输出流程等多块知识。真正把它应用到实际项目时要记住三个判断全局像素化优先用 RenderTexture Point 过滤开销低且代码稳定。需要动态调节像素强度或处理局部特效时用后处理 Shader但要处理 URP 兼容问题。粒子系统和 VFX Graph 要做到风格统一必须检查和统一纹理过滤模式。后续可以继续深入的方向包括像素化 边缘发光、像素化 颜色量化减少色阶实现复古感、按深度缓冲做像素化遮挡、在不同渲染管线下的 Renderer Feature 实现。建议先从本文的三种方案里选一个跑通再逐步叠加你自己的创意这比一次性追求大型组件更有效。
返回列表