4、片元着色器:指令数与带宽的博弈,MAD与Tex指令的平衡艺术
片元着色器,说白了就是GPU里最累的那个角色。
每个像素都要跑一遍,分辨率越高,调用次数越恐怖。我见过不少项目,顶点着色器优化得漂漂亮亮,结果一到片元阶段就崩了——帧率直接腰斩。嗯,这里要重点聊聊。
4.1 指令数:不是越少越好
很多人有个误区:片元着色器的指令数越少,性能就越好。
其实不完全对。指令数少当然好,但关键要看是什么指令。
我个人习惯把片元着色器的指令分成两类:
- 算术指令(MAD):加减乘除、点积、三角函数等
- 纹理指令(TEX):采样、读取纹理数据
这两者的代价完全不同。MAD指令在GPU里跑得飞快,一个时钟周期能执行好几个。但TEX指令就不一样了——它要访问显存,要等带宽,延迟高得吓人。
核心原则:一条TEX指令的代价,约等于4-8条MAD指令。所以别光盯着指令总数,要看TEX指令的数量。
4.2 带宽:真正的瓶颈
我在项目中遇到过最典型的案例:一个后处理特效,片元着色器里采样了6次纹理。当时在高端机上跑得还行,一放到中端机,帧率直接掉到20帧。
为什么会这样?
因为带宽不够了。移动端的GPU,带宽就那么点。你采一次纹理,就要从显存里读一次数据。6次采样,带宽压力翻倍。
我当时的解决方案很简单:把6次采样合并成3次,用数学计算来弥补缺失的信息。效果差了一点点,但帧率从20帧回到了55帧。值不值?太值了。
避坑指南:我曾经为了省一条MAD指令,多采了一次纹理。结果性能反而下降了。记住:宁肯多算,不要多读。
4.3 MAD与TEX的平衡艺术
那到底怎么平衡?我总结了一套经验:
| 场景 | 建议策略 | 原因 |
|---|---|---|
| 简单颜色计算 | 多用MAD,少用TEX | MAD延迟低,带宽压力小 |
| 复杂光照计算 | 适当增加TEX,减少MAD | 预计算纹理可以省掉大量算术 |
| 后处理特效 | 优先减少TEX次数 | 后处理通常采样密集,带宽是瓶颈 |
| UI渲染 | MAD和TEX都尽量少 | UI简单,没必要复杂计算 |
你想想看,一个光照计算,如果用数学公式算,可能要十几条MAD指令。但如果把光照结果预计算到一张纹理里,一次TEX采样就搞定了。哪个划算?
嗯,要看具体情况。如果纹理很小,带宽压力不大,那TEX更划算。如果纹理很大,或者采样次数很多,那MAD反而更好。
4.4 实战技巧:如何优化
我分享几个实战中常用的技巧:
- 纹理压缩:用ETC2或ASTC压缩纹理,减少带宽占用。我见过有人用RGBA8888存法线贴图,其实RGBA4444就够了,带宽直接减半。
- LOD控制:远处的物体用低分辨率纹理。别让片元着色器去采高精度的纹理,浪费。
- 提前退出:如果像素的alpha值接近0,直接discard掉,别继续算了。我有个项目,加了这行判断,帧率提升了15%。
- 合并采样:能用一次采样搞定的,别分两次。比如把颜色和法线打包到一张纹理里。
注意:discard指令虽然能提前退出,但它会破坏GPU的Early-Z优化。用的时候要小心,别因小失大。
4.5 代码示例:优化前后对比
来看一个实际例子。这是一个简单的光照计算:
// 优化前:6次纹理采样
vec4 color = texture2D(uDiffuse, uv);
vec4 normal = texture2D(uNormal, uv);
vec4 specular = texture2D(uSpecular, uv);
vec4 emissive = texture2D(uEmissive, uv);
vec4 ao = texture2D(uAO, uv);
vec4 roughness = texture2D(uRoughness, uv);
// 优化后:3次纹理采样,合并数据
vec4 packed1 = texture2D(uPacked1, uv); // 颜色+粗糙度
vec4 packed2 = texture2D(uPacked2, uv); // 法线+AO
vec4 packed3 = texture2D(uPacked3, uv); // 高光+自发光
vec4 color = vec4(packed1.rgb, 1.0);
float roughness = packed1.a;
vec3 normal = packed2.rgb;
float ao = packed2.a;
vec3 specular = packed3.rgb;
float emissive = packed3.a;
你看,纹理采样从6次降到了3次。代价是多了一些解包的计算(几条MAD指令)。但这点计算代价,跟带宽节省比起来,根本不值一提。
我记得当时在骁龙865上测试,优化前帧率38帧,优化后57帧。效果几乎没变,性能提升50%。
4.6 总结一下
片元着色器的优化,说白了就是一场博弈。你要在指令数和带宽之间找到平衡点。
- MAD指令便宜,但别滥用
- TEX指令贵,但有时候不得不采
- 关键看你的瓶颈在哪——是计算能力还是带宽
我个人建议:先做性能分析,找到瓶颈,再决定是减MAD还是减TEX。别盲目优化,有时候你省了一条MAD,反而多采了一次纹理,得不偿失。
嗯,这一章就到这里。下一章我们聊聊顶点着色器的优化,那个又是另一番天地了。