ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

后处理实现细节:从“整张画面“到“逐像素加工“的完整拆解

后处理实现细节:从“整张画面“到“逐像素加工“的完整拆解 开场一个效果出来了但不懂原理的困惑小王照着教程加了个 Bloom效果是出来了但他心里没底“我知道后处理是’加工整张画面’……但具体是怎么加工的画面怎么就’交’给我的 Shader 了我的 Shader 又怎么’画’回屏幕那个Blit、_MainTex、全屏三角形……到底在干嘛”老鸟说“好问题只会调参数不算懂。今天把后处理从头到尾的实现链路拆给你看——你会发现它本质就是一次’画满屏幕的贴图操作’” 第一幕后处理的核心机制——Blit什么是 BlitBlit 把一张纹理,用某个Shader,画到另一张纹理上 这是后处理最核心的操作! ↓ 输入: 源纹理(当前画面) 处理: 一个后处理Shader 输出: 目标纹理(处理后的画面)Blit 的本质画一个铺满屏幕的矩形Blit具体在干嘛 1. 画一个铺满整个屏幕的矩形(全屏Quad) 2. 把源纹理当贴图贴上去 3. 用后处理Shader处理每个像素 4. 结果输出到目标纹理 ↓ 本质: 就是画一个贴了图的全屏矩形!生动比喻投影加滤镜Blit像把画面投到墙上,中间加片滤镜 源纹理(原画面) 幻灯片 后处理Shader 中间的滤镜片 目标纹理 墙(投出的结果) ↓ 光穿过滤镜,墙上就是处理后的画面 ↓ 画全屏矩形 让整面墙都被投到 第二幕全屏矩形是怎么来的后处理不需要普通的3D模型普通渲染: 顶点着色器处理3D模型的顶点 后处理: 只需要一个铺满屏幕的平面 不需要复杂模型! ↓ 就是屏幕大小的一个矩形(或三角形) 覆盖整个屏幕即可方式1全屏四边形(Quad)用2个三角形拼成一个矩形,盖满屏幕 (-1,1)────(1,1) │ ╲ │ │ ╲ │ │ ╲ │ (-1,-1)───(1,-1) ↓ 4个顶点,覆盖整个屏幕(NDC坐标-1到1)方式2全屏三角形(更优)⭐用1个超大三角形盖住屏幕(现代做法) (-1,3) │╲ │ ╲ │ ╲ (-1,-1)──(3,-1) ← 超出屏幕 ↓ 1个大三角形,超出屏幕的部分被裁掉 剩下正好盖满屏幕 ↓ 比四边形少1个顶点,还避免了 两个三角形交界处的重复计算 ↓ 现代引擎(URP)多用这个!生动理解为什么三角形更优四边形(2三角形): 中间对角线处 相邻像素可能被处理两次(边界重复) 大三角形(1个): 没有内部边界 每个像素只处理一次 ↓ 一个大三角形更高效! (这是个小而精妙的优化) 第三幕后处理 Shader 的结构顶点着色器几乎什么都不做后处理的顶点着色器超简单 v2f vert(appdata v) { v2f o; // 顶点已是屏幕空间,直接输出 o.pos v.vertex; // 传递UV(用来采样源纹理) o.uv v.uv; return o; } ↓ 不用做复杂的3D变换! 顶点就是全屏矩形的角片元着色器真正干活的地方每个像素运行一次,处理颜色 half4 frag(v2f i) : SV_Target { // 采样源纹理(当前画面的这个像素) half4 color tex2D(_MainTex, i.uv); // 在这里加工! (调色、模糊等) color.rgb * 1.2; // 例:提亮 return color; // 输出处理后的颜色 } ↓ _MainTex就是输入的画面 i.uv是当前像素在画面上的位置关键_MainTex 和 uv_MainTex: Blit时自动传入的源画面纹理 你要处理的那张画面 i.uv: 当前像素的纹理坐标(0~1) (0,0)左下角 (1,1)右上角 ↓ tex2D(_MainTex, i.uv) 取出画面上这个位置的原始颜色 ↓ 然后你对它做各种加工! 第四幕完整实现一个后处理调色最简单的后处理整体调色Shader PostProcess/ColorAdjust { Properties { _MainTex (Texture, 2D) white {} _Brightness (Brightness, Float) 1.0 _Saturation (Saturation, Float) 1.0 } SubShader { // 后处理不需要深度测试、剔除 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 pos : SV_POSITION; }; sampler2D _MainTex; float _Brightness; float _Saturation; v2f vert(appdata v) { v2f o; o.pos UnityObjectToClipPos(v.vertex); o.uv v.uv; return o; } half4 frag(v2f i) : SV_Target { // 1. 采样原始画面 half4 color tex2D(_MainTex, i.uv); // 2. 亮度调整 color.rgb * _Brightness; // 3. 饱和度调整 half gray dot(color.rgb, half3(0.299, 0.587, 0.114)); color.rgb lerp(gray.xxx, color.rgb, _Saturation); return color; } ENDCG } } }关键点解读Cull Off ZWrite Off ZTest Always 后处理是全屏矩形,不需要: - 剔除(Cull Off) - 写深度(ZWrite Off) - 深度测试(ZTest Always,总是通过) ↓ 这几行是后处理Shader的标配! 采样 → 加工 → 输出 frag里的三步就是后处理核心逻辑️ 第五幕C# 端如何驱动后处理老式做法OnRenderImage// 内置管线的经典后处理入口publicclassPostEffect:MonoBehaviour{publicMaterialpostMaterial;// 系统会自动调用,src是当前画面voidOnRenderImage(RenderTexturesrc,RenderTexturedst){// 把src用postMaterial处理,输出到dstGraphics.Blit(src,dst,postMaterial);// ↑ 这就是那个核心Blit操作!}}OnRenderImage流程 src 场景渲染好的画面(自动传入) Blit(src, dst, mat) 用材质处理src→dst dst 最终显示的画面 ↓ 系统自动把dst显示到屏幕多个效果串联多次 BlitvoidOnRenderImage(RenderTexturesrc,RenderTexturedst){// 申请临时RT做中间结果RenderTexturetempRenderTexture.GetTemporary(src.width,src.height);// 第1个效果: src → tempGraphics.Blit(src,temp,blurMaterial);// 第2个效果: temp → dstGraphics.Blit(temp,dst,colorMaterial);// 释放临时RTRenderTexture.ReleaseTemporary(temp);}⚠️ 注意: 每个Blit都是一次RT切换! 多个效果 多次切换 移动端贵! (所以前面讲要合并效果)现代做法URP 的 Render FeatureURP用ScriptableRenderPass - 继承ScriptableRenderPass - 在Execute里用CommandBuffer.Blit - 通过Render Feature注入管线 ↓ 更灵活,能精确控制在管线哪个阶段插入 更好地管理RT和Load/Store ↓ 移动端优先用URP的方式! 第六幕进阶——Bloom 的完整实现逻辑Bloom 的多步流程Bloom(泛光)不是一次Blit能搞定的 1. 提取亮部(阈值) 只保留很亮的像素 2. 降分辨率 模糊(多次) 在小图上高斯模糊(省!) 3. 逐级模糊(金字塔) 多个分辨率级别模糊叠加 4. 合成回原图 模糊的亮部 原画面 ↓ 多步 多次Blit(要合理管理)Bloom 实现拆解详细步骤(每步一次Blit) 原图 ↓ 提取亮部Shader(降到1/2分辨率) 亮部图(小) ↓ 水平模糊Shader ↓ 垂直模糊Shader (高斯分两次做,更快) 模糊亮部 ↓ (可能逐级降采样再升采样) ↓ 合成Shader(原图 模糊亮部) 最终Bloom效果 ↓ 关键优化: 在低分辨率做模糊!高斯模糊分离优化✅ 二维高斯模糊 水平模糊 垂直模糊 ❌ 直接2D模糊: N×N次采样(如9×981次) ✅ 分离成两次1D: NN次(如9918次) ↓ 一个像素采样从81次降到18次! ↓ 这是模糊类后处理的关键优化生动理解分离模糊2D模糊像擦一个方形区域 要擦N×N个格子 分离模糊像先横擦一道,再竖擦一道 横擦N个 竖擦N个 两道横竖交叉,效果等价于方形! ↓ 从乘法变加法,省一大截! 第七幕后处理数据流全景图完整数据流【单个效果】 场景画面(RT) ↓ Blit(全屏三角形 后处理Shader) frag: 采样_MainTex → 加工 → 输出 处理后画面(RT) → 屏幕 【多个效果(未合并)】 场景 → Blit效果1 → RT1 → Blit效果2 → RT2 → 屏幕 (切换!) (切换!) ↓ 每次Blit都是RT切换,移动端贵! 【多个效果(合并优化)】 场景 → Blit(一个Uber Shader做完多效果) → 屏幕 (只切换一次!) ↓ 移动端推荐!关键概念串联Blit 用Shader把纹理画到纹理 全屏三角形 覆盖屏幕的绘制载体 _MainTex 输入的源画面 frag采样加工 逐像素处理逻辑 临时RT 中间结果存放 RT切换 每次Blit的隐藏开销(移动端痛) ↓ 理解这几个就理解了后处理实现!✅ 后处理实现理解检查清单核心机制 □ 明白Blit是用Shader把纹理画到纹理 □ 明白后处理是画全屏矩形/三角形 □ 明白全屏三角形比四边形优在哪 Shader结构 □ 明白顶点着色器几乎不做事 □ 明白frag里采样→加工→输出三步 □ 明白_MainTex是输入画面 □ 明白i.uv是像素位置 □ 知道Cull Off ZWrite Off ZTest Always C#驱动 □ 明白OnRenderImage/Blit的作用 □ 明白多效果多次Blit多次切换 □ 知道URP用ScriptableRenderPass 进阶 □ 明白Bloom是多步流程 □ 明白高斯模糊分离优化(2D→1D×2) □ 明白低分辨率做模糊的意义 一句话总结后处理的实现本质它就是一次Blit操作——画一个铺满屏幕的三角形或矩形把当前画面当作纹理_MainTex贴上去用后处理 Shader 逐像素加工结果输出到目标纹理。Shader 结构极简顶点着色器几乎不做事片元着色器做核心三步——采样 _MainTex、加工、输出。C# 端通过 OnRenderImage/Blit或 URP 的 RenderPass驱动多个效果 多次 Blit 多次 RT 切换移动端要合并优化复杂效果如 Bloom 是多步流程靠低分辨率和分离模糊来省性能。核心口诀后处理即Blit画个全屏三角形采样_MainTex加工输出多效果合并减切换模糊分离降分辨率 后处理实现要点速查表概念作用要点Blit纹理→Shader→纹理后处理核心操作全屏三角形覆盖屏幕的载体比四边形优_MainTex输入源画面Blit自动传入i.uv像素位置(0~1)用于采样frag三步采样→加工→输出核心逻辑Cull/ZWrite Off后处理标配不需深度剔除临时RT中间结果用完Release分离模糊2D→1D×2采样量骤减降分辨率模糊类优化反正是糊的 一句话记住核心后处理 “拿整张画面当贴图画个全屏三角形逐像素重新加工”。理解了 Blit 这个核心操作所有后处理调色、模糊、Bloom都只是frag 里加工逻辑的不同而已 延伸从实现细节回看优化【实现细节解释了为什么要那样优化】 现在你懂了实现,就懂了优化的道理: 合并效果 → 因为每次Blit都要RT切换 合并 少切换 少搬运带宽 ✓ 降分辨率 → 因为frag要处理每个像素 小图 像素少 采样少 ✓ 分离模糊 → 因为2D采样量是平方级 分离 采样从乘法变加法 ✓ 用LUT → 因为frag里复杂计算贵 LUT 复杂计算变一次采样 ✓ ↓ 理解实现 → 优化不再是死记,而是自然推导!
返回列表