尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Shader 优化计算:榨干每一滴 GPU 性能

Shader 优化计算:榨干每一滴 GPU 性能 开场一个每秒算几十亿次的战场小王写了个漂亮的水面 ShaderPC 上流畅得很。一放到手机上——帧率暴跌手机发烫烫手他很委屈“我代码逻辑没错啊为什么这么卡”老鸟拿过来一看摇摇头“逻辑没错但你的 Shader里塞满了昂贵的计算。你要明白——这段代码每一帧、每个像素都要跑一遍”“一个 1080p 屏幕 200万像素每秒 60 帧每秒要跑 1.2 亿次你的片元着色器你多写一个昂贵计算就乘以 1.2 亿”小王倒吸一口凉气“原来 Shader 优化是在跟’亿次’较劲” 第一幕为什么 Shader 优化如此重要核心认知Shader 是海量并行执行的你的Shader代码不是跑一次 顶点着色器每个顶点跑一次 (模型10万顶点 跑10万次) 片元着色器每个像素跑一次 (1080p 200万像素/帧) (60帧 每秒1.2亿次!) ↓ 你省下1条指令 → 省下上亿次计算 你多写1条昂贵指令 → 增加上亿次负担生动比喻给一亿人发东西Shader优化就像给一亿人发传单 传单上多印一个字(多一次计算) 要多印一亿个字 ↓ 在这种规模下 省一点点 × 一亿 省很多 浪费一点点 × 一亿 灾难 ↓ 所以Shader里斤斤计较是值得的!优化的两个战场【顶点着色器】 执行次数 顶点数(相对少) 优化重点能在顶点算的别放片元 【片元着色器】 执行次数 像素数(海量!) 优化重点这是主战场重点开刀! ↓ 核心策略把计算从片元挪到顶点 或挪到CPU/预计算 第二幕优化铁律——计算搬家铁律1能预计算的别在Shader里算计算频率从低到高 预计算(离线) → 一次 CPU每帧 → 每帧几次 顶点着色器 → 每顶点(几万次) 片元着色器 → 每像素(几百万次!) ↓ 能往低频挪就往低频挪!生动理解计算搬家就像做菜准备食材 片元着色器里算 每上一道菜现切菜 (客人多时忙死) 预计算好 提前把菜切好备着 (上菜时直接用) ↓ 把重复不变的计算提前做好 Shader里直接用结果铁律2顶点能算的别放片元✅ 经典优化计算挪到顶点着色器 顶点着色器算 → 每顶点算一次(几万次) ↓ GPU自动插值 片元拿到插值结果 → 直接用 对比 片元里算 → 每像素算(几百万次) ↓ 能挪到顶点省下几十倍计算!例子光照计算的取舍逐顶点光照(Gouraud): 顶点算光照 → 插值到片元 便宜! 但精度低(高光可能不准) 逐像素光照(Phong): 片元算光照 贵! 但精度高(高光准确) ↓ 移动端/远处物体用逐顶点 近处重要物体用逐像素 ↓ 按需选择不要一律逐像素 第三幕昂贵操作黑名单昂贵操作1复杂数学函数❌ 昂贵的数学函数 sin, cos, tan (三角函数) pow (幂运算) exp, log (指数对数) sqrt (开方) normalize (归一化,含开方) ↓ 这些比加减乘除贵很多!✅ 优化手段 - 能避免就避免 - 用查找表(纹理)代替 - 用廉价近似替代 - 合并同类计算昂贵操作2除法❌ 除法比乘法贵 a / b → 较慢✅ 优化 预计算倒数改用乘法 float invB 1.0 / b; // 算一次 a * invB; // 之后用乘法 或多个除以同一个数 (a c d) / b → (a c d) * (1.0/b) // 只除一次昂贵操作3分支(if/else)❌ 动态分支可能很贵 GPU是一群线程一起走的 if (条件) // 不同线程条件可能不同 { 贵计算A } else { 贵计算B } ↓ 最坏情况A和B都要算! (因为线程们步调不一致)✅ 优化分支 - 用数学代替分支 step()、lerp()、min/max - 例: if(x0) a else b → lerp(b, a, step(0, x)) - 让分支条件在一批内一致 (uniform分支比动态分支便宜)昂贵操作4纹理采样❌ 纹理采样有开销(尤其带宽) 每次tex2D()都要 - 访问显存(带宽) - 可能的过滤计算 ↓ 采样次数多 带宽压力大 移动端带宽宝贵!✅ 优化采样 - 减少采样次数 - 合并多张贴图到一张(打包通道) 如R存金属度, G存粗糙度, B存AO - 用合适的贴图分辨率(别过大) - 注意dependent texture read(依赖采样)昂贵操作黑名单总结按贵的程度(移动端): 分支(处理不当) ★★★★★ 复杂函数(sin/pow/exp) ★★★★ 纹理采样(带宽) ★★★★ 除法 ★★★ normalize/sqrt ★★★ 乘法 ★ 加减 ★ 第四幕精度优化——移动端的关键精度类型float / half / fixedShader里数据精度可选 float (高精度, 32位) → 慢, 占带宽 half (中精度, 16位) → 快, 移动端友好 fixed (低精度, 常见于颜色) → 最快 ↓ 移动端: 能用half就别用float!生动理解精度精度就像用多大的秤 float 精密电子秤(测细菌都行) 但慢、贵 half 普通厨房秤 够用快便宜 ↓ 称白菜用不着测细菌的秤! 颜色、方向等用half足够精度选择原则✅ 用half的场景 - 颜色(0~1范围, 精度够) - 法线、方向 - UV(小范围时) - 大部分中间计算 ⚠️ 需要float的场景 - 世界坐标(数值大, 需要精度) - 时间累积 - 深度相关精确计算精度优化例子❌ 全用float float4 color tex2D(...); float3 normal ...; ✅ 该用half用half half4 color tex2D(...); // 颜色half够 half3 normal ...; // 法线half够 ↓ 移动端性能明显提升 带宽压力减小️ 第五幕具体优化技巧集技巧1MAD 合并乘加✅ GPU对乘加有硬件优化 a * b c → 一条MAD指令(超快) 所以尽量凑成乘加形式 ❌ (a b) * c → 展开成 a*c b*c 如果能凑MAD更好技巧2向量化计算✅ 一次算多个分量 GPU擅长向量运算(4个一起算): ❌ 分开算: float r ...; float g ...; float b ...; ✅ 打包算: float3 rgb ...; // 一次算3个 ↓ 充分利用GPU的向量单元技巧3用查找表(LUT)代替计算✅ 复杂计算 → 预算进纹理 比如复杂的颜色映射、曲线 把结果预计算存进一张纹理 ↓ Shader里采样纹理取结果 用一次采样代替一堆计算 ↓ Color Grading(颜色分级)常用此法技巧4减少插值变量✅ 顶点传给片元的变量要精简 顶点→片元的插值变量(varying) 每个都占带宽、占插值器资源 ↓ - 合并能合并的(打包进float4) - 删掉用不到的 - 减少数量技巧5避免动态循环❌ 循环次数不确定 for (int i 0; i 动态变量; i) ✅ 用固定次数(能展开): for (int i 0; i 4; i) // 编译期展开 ↓ 固定循环编译器能优化展开 动态循环开销大技巧6discard 要谨慎⚠️ discard(Alpha Test)的代价 discard会破坏Early-Z / HSR! (前面讲过) ↓ - 移动端尽量少用 - 必须用时放对渲染队列 - 能用实体网格代替就代替 第六幕如何定位 Shader 瓶颈判断是顶点瓶颈还是片元瓶颈测试方法 改变屏幕分辨率 分辨率降低帧率明显上升 → 片元着色器瓶颈(像素相关) 改变模型面数/数量 面数降低帧率明显上升 → 顶点着色器瓶颈(顶点相关) ↓ 定位了瓶颈才知道优化哪里工具1GPU ProfilerUnity Profiler → GPU模块 或平台专用工具 - Xcode GPU工具(iOS) - Snapdragon Profiler(Android) - RenderDoc(通用抓帧) ↓ 看每个DrawCall/Shader的GPU耗时工具2Frame Debugger看渲染每一步 分析哪个Pass/Shader耗时工具3Overdraw 视图Scene视图 → 渲染模式 → Overdraw 看哪里Overdraw严重(颜色越红越浪费) ↓ 透明物体、粒子重叠区域 往往是Overdraw重灾区优化流程1. Profiler确认是GPU瓶颈 2. 判断顶点瓶颈还是片元瓶颈 3. 对症下药: - 片元瓶颈 → 简化片元计算/降Overdraw - 顶点瓶颈 → 简化模型/减顶点计算 4. 改完再测对比效果 ↓ 测量-优化-再测量循环⚠️ 第七幕优化误区误区1过早优化❌ 一上来就死抠每条指令 正确顺序 先保证功能正确 用Profiler找到真正的瓶颈 再针对瓶颈优化 ↓ 不要优化不是瓶颈的地方(浪费精力)误区2盲目相信少代码快❌ 代码短就一定快 不一定 - 一次纹理采样(1行) 可能比 十几行数学 更贵 - 关键看指令的代价不是行数 ↓ 要理解每种操作的真实开销误区3忽视平台差异❌ PC上快 手机上快 大错特错! - 移动GPU弱很多 - 带宽是移动端大瓶颈 - 精度(half)在移动端影响大 ↓ 一定要在目标设备上实测!误区4牺牲太多画质❌ 为性能把画质砍到没法看 优化是性价比 用最小的画质代价换最大的性能 ↓ 找到画质和性能的平衡点 不是一味求快✅ Shader 优化检查清单计算搬家 □ 能预计算的移到CPU/离线了吗 □ 顶点能算的没放片元吗 □ 逐像素计算是否必要(能否逐顶点) 昂贵操作 □ 减少了sin/cos/pow/exp等 □ 除法改成乘法了吗 □ 分支能用数学(lerp/step)代替吗 □ 纹理采样次数最小化了吗 精度优化 □ 移动端该用half的用half了吗 □ 只在必要处用float 技巧应用 □ 用了向量化计算 □ 复杂映射用LUT纹理了吗 □ 插值变量精简了吗 □ 避免了动态循环 □ 谨慎使用discard 定位验证 □ 确认是GPU瓶颈 □ 分清顶点/片元瓶颈 □ 在目标设备上实测了 □ Overdraw检查了吗 一句话总结Shader 优化计算的核心永远记住这段代码每帧每像素跑上亿次核心策略是计算搬家——能预计算就别实时算能顶点算就别片元算。警惕昂贵操作复杂函数、除法、分支、纹理采样移动端善用低精度half用向量化、LUT、MAD 等技巧榨干性能。先用 Profiler 定位真正瓶颈在目标设备上实测找到画质与性能的最佳平衡点核心口诀每帧跑上亿计算要搬家贵函数少用精度用half先测再优化实机才算数 优化优先级速查表优化手段收益难度优先级降低 Overdraw极高中⭐⭐⭐⭐⭐计算挪到顶点/CPU高中⭐⭐⭐⭐⭐减少纹理采样高中⭐⭐⭐⭐用 half 精度高(移动)低⭐⭐⭐⭐减少昂贵函数中高中⭐⭐⭐⭐除法改乘法中低⭐⭐⭐分支优化中中⭐⭐⭐向量化中低⭐⭐⭐ 记住黄金法则80% 的性能问题来自 20% 的代码。用 Profiler 找到那 20%集中火力优化而不是平均用力抠每一行 延伸现代 Shader 优化新方向【趋势变化】 1. Shader变体(Variant)管理 过多变体导致编译慢、包体大 → 精简变体善用#pragma 2. 计算着色器(Compute Shader) 把并行计算搬到GPU → 粒子、物理、大规模数据处理 3. 移动端专项 TBDR架构特性利用 → 减少带宽、善用片上缓存 4. 可变分辨率渲染 重要区域高清边缘降分辨率 → 省下大量片元计算告诉我方向
返回列表