最近工作中,未了进一步提升美术渲染效果,不得已我们需要从数学的角度优化我们的图形渲染,减少不必要的ALU和MUL,从而提升运行效率。提供更多的渲染效果支持。 当然,虽然我们游戏现在发热已经控制的比较完美了 ,但是我们还能从硬件级优化。
接下来就是我这段时间用了半斤头发 研究出来的方案 。 绝对干货,优化图形这块照搬即可。
总结一下,可能的优化步骤包括:
- 减少复杂数学运算,使用近似或预计算。
- 优化向量化运算,利用SIMD。
- 避免或减少条件分支,使用数学函数替代。
- 缓存中间计算结果,避免重复计算。
- 使用适当的精度(half/fixed)代替float。 ======》比较常见
- 减少纹理采样次数,优化纹理使用。
- 使用内置优化函数和宏。
- 管理着色器变体,减少不必要的变体。
- 优化光照计算,简化或预计算部分光照模型。
- 使用工具分析性能瓶颈,针对性优化。
需要在实际的着色器中找到具体的瓶颈,比如通过Frame Debugger查看某个Pass的GPU耗时,或者通过查看编译后的着色器代码中的指令数,确定哪里可以优化。
举个例子,假设有一个计算漫反射的Lambert光照模型:
float diff = max(0, dot(normal, lightDir));
float3 diffuse = diff * lightColor * albedo;
这里,max函数可以替换成使用内置的saturate(如果dot结果在-1到1之间),或者使用DotClamped(如果URP有类似的内置函数)。另外,如果lightColor和albedo都是float3,那么向量化的乘法可能更高效。
另一个例子是镜面高光的Phong或Blinn-Phong模型:
float3 viewDir = normalize(viewPos - fragPos);
float3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
float3 specular = spec * lightColor;
这里,pow计算可能比较昂贵,尤其是当指数较大时。可以考虑使用近似计算,或者将指数值限制在某个范围内。或者,使用Blinn-Phong模型中的半角向量,可能会减少计算步骤:
float3 halfDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(normal, halfDir), 0.0), 32);
不过,这样替换后是否更高效?可能还需要测试。或者,是否可以用其他函数替代pow,比如使用exp2和log2的组合(复杂的组合可能还不如pow,所以需要按实际计算公司来搭配),或者查找表。
另外,normalize函数内部需要进行长度的计算和除法,比较消耗ALU。如果可能的话,避免多次normalize,而是将中间结果保存下来。比如,如果lightDir和viewDir已经在前面被normalize过,那么后面就不需要再次normalize。
此外,如果多个计算步骤中有重复的向量运算,比如多次计算dot(normal, lightDir),可以将其结果保存到变量中重复使用。
在URP中,可能还需要注意屏幕空间阴影、屏幕空间环境光遮蔽(SSAO)等后处理效果的计算,这些通常涉及较多的ALU操作。优化这些效果的方法可能包括降低采样分辨率,使用更高效的算法,或者调整参数以减少计算量。
此外,对于透明度处理,如alpha blending,可能需要较多的混合操作,但这也属于ROP(光栅化操作)的优化范畴,而ALU优化更侧重于计算部分。
总之,具体的优化策略需要根据实际的着色器代码和性能分析结果来制定。可能需要逐步测试和调整,确保优化后的效果在视觉上没有明显差异,同时提升性能。
在Unity URP渲染管线中进行ALU优化可以显著提升GPU性能,尤其是在移动设备上。以下是分步的优化策略:
ALU核心优化 ★★★★★
在移动端优化Unity URP渲染管线时,由于移动GPU的ALU(算术逻辑单元)性能、带宽和功耗限制更为严格,需要采取更极端的优化策略。以下是针对移动端的详细优化方法和示例:
移动端GPU特性与挑战
- 架构特点:
- Tile-Based Rendering(如Mali、Adreno):分块渲染,对带宽敏感。
- 低ALU吞吐量:相比桌面GPU,移动端ALU性能更弱。
- 高功耗限制:复杂计算会导致发热和降频。
- 核心优化目标:
- 减少片段着色器(Fragment Shader)的ALU指令。
- 降低带宽占用(纹理采样、顶点数据)。
- 避免分支和复杂循环。
ALU优化核心思路
- 减少重复计算:缓存中间结果,避免重复运算。
- 简化数学操作:用近似公式或低精度类型替代高开销运算。
- 减少分支和循环:避免GPU的线程分歧(Thread Divergence)。
- 利用内置函数和硬件特性:如
mad
(乘加)、rsqrt
等。
优化技巧与示例
1. 数据类型优化
- 优先使用低精度类型:
half
(16位浮点)代替float
(32位)。fixed
(低精度,适用于颜色和归一化值)。-
// 错误:使用float计算颜色 float3 color = _MainTex.Sample(uv) * 2.0; // 正确:使用half或fixed half3 color = _MainTex.Sample(uv) * 2.0h;
2. 数学运算简化
- 用近似公式代替精确计算:
- 例如:用
1.0 / (1.0 + x)
代替exp(-x)
(菲涅尔近似)。 - 避免
pow
、sin
等高开销函数,用查表(LUT)或多项式近似替代。
- 例如:用
-
// 高开销的精确计算 half specular = pow(max(0, dot(N, H)), _Gloss); // 优化:用近似公式或查表 half roughness = 1.0 - _Gloss; half specular = exp2((-roughness * roughness) * 10.0);
3. 减少纹理采样
- 合并纹理通道:
- 将多个数据打包到单张纹理的RGBA通道(如金属度、粗糙度、AO)。
- 使用Mipmap和压缩格式:
- 强制启用纹理Mipmap,使用ASTC压缩格式。
-
// 原始:分别采样金属度、粗糙度、AO half metallic = tex2D(_MetallicTex, uv).r; half roughness = tex2D(_RoughnessTex, uv).r; half ao = tex2D(_AOTex, uv).r; // 优化:合并到单张纹理的RGB通道 half3 materialData = tex2D(_MaterialMap, uv).rgb; half metallic = materialData.r; half roughness = materialData.g; half ao = materialData.b;
4. 避免分支与循环
- 完全消除
if
语句:- 用
step()
、lerp()
或数学运算替代条件判断。
- 用
- 手动展开短循环:
- 避免动态循环次数,固定次数循环直接展开。
-
// 低效:移动端GPU分支性能极差 if (uv.x > 0.5) { color = _ColorA; } else { color = _ColorB; } // 优化:用step和lerp替代 half mask = step(0.5h, uv.x); color = lerp(_ColorB, _ColorA, mask);
包括三目运算发,也会比if else 快。
在移动端优化Unity URP渲染管线时,由于移动GPU的ALU(算术逻辑单元)性能、带宽和功耗限制更为严格,需要采取更极端的优化策略。以下是针对移动端的详细优化方法和示例:
一、移动端GPU特性与挑战
- 架构特点:
- Tile-Based Rendering(如Mali、Adreno):分块渲染,对带宽敏感。
- 低ALU吞吐量:相比桌面GPU,移动端ALU性能更弱。
- 高功耗限制:复杂计算会导致发热和降频。
- 核心优化目标:
- 减少片段着色器(Fragment Shader)的ALU指令。
- 降低带宽占用(纹理采样、顶点数据)。
- 避免分支和复杂循环。
二、移动端ALU优化策略与示例
1. 数据类型优化
- 优先使用低精度类型:
half
(16位浮点)代替float
(32位)。fixed
(低精度,适用于颜色和归一化值)。
- 示例:
glsl
// 错误:使用float计算颜色 float3 color = _MainTex.Sample(uv) * 2.0; // 正确:使用half或fixed half3 color = _MainTex.Sample(uv) * 2.0h;
2. 数学运算简化
- 用近似公式代替精确计算:
- 例如:用
1.0 / (1.0 + x)
代替exp(-x)
(菲涅尔近似)。 - 避免
pow
、sin
等高开销函数,用查表(LUT)或多项式近似替代。
- 例如:用
- 示例:简化光照计算中的
pow
:glsl
// 高开销的精确计算 half specular = pow(max(0, dot(N, H)), _Gloss); // 优化:用近似公式或查表 half roughness = 1.0 - _Gloss; half specular = exp2((-roughness * roughness) * 10.0);
3. 减少纹理采样
- 合并纹理通道:
- 将多个数据打包到单张纹理的RGBA通道(如金属度、粗糙度、AO)。
- 使用Mipmap和压缩格式:
- 强制启用纹理Mipmap,使用ASTC压缩格式。
- 示例:合并数据到单张纹理:
glsl
// 原始:分别采样金属度、粗糙度、AO half metallic = tex2D(_MetallicTex, uv).r; half roughness = tex2D(_RoughnessTex, uv).r; half ao = tex2D(_AOTex, uv).r; // 优化:合并到单张纹理的RGB通道 half3 materialData = tex2D(_MaterialMap, uv).rgb; half metallic = materialData.r; half roughness = materialData.g; half ao = materialData.b;
4. 避免分支与循环
- 完全消除
if
语句:- 用
step()
、lerp()
或数学运算替代条件判断。
- 用
- 手动展开短循环:
- 避免动态循环次数,固定次数循环直接展开。
- 示例:消除分支:
glsl
// 低效:移动端GPU分支性能极差 if (uv.x > 0.5) { color = _ColorA; } else { color = _ColorB; } // 优化:用step和lerp替代 half mask = step(0.5h, uv.x); color = lerp(_ColorB, _ColorA, mask);
5. 光照计算的极致优化
- 简化光照模型:
- 使用Lambert代替GGX(移动端PBR可简化)。
- 预计算环境光(IBL)到球谐(SH)或LUT。
-
// 常规PBR的GGX计算(高开销) half D_GGX(half NdotH, half roughness) { half a = roughness * roughness; half a2 = a * a; half denom = (NdotH * a2 - NdotH) * NdotH + 1.0; return a2 / (PI * denom * denom); } // 移动端优化:近似GGX half D_Approx(half NdotH, half roughness) { half a = roughness * roughness; return a / (4.0 * PI * pow(NdotH * NdotH * (a - 1.0) + 1.0, 2)); }
6. 顶点着色器预处理
- 将计算从片段着色器迁移到顶点着色器:
- 例如:预计算光照方向、雾效强度等。
// 顶点着色器
v2f vert(appdata v) {
v2f o;
o.pos = TransformObjectToHClip(v.vertex);
o.worldNormal = TransformObjectToWorldNormal(v.normal);
o.lightDir = WorldSpaceLightDir(v.vertex); // 预计算光照方向
return o;
}
// 片段着色器直接使用预计算值
half4 frag(v2f i) : SV_Target {
half3 lightDir = normalize(i.lightDir);
half3 normal = normalize(i.worldNormal);
half ndotl = saturate(dot(normal, lightDir));
// ...
}
三、移动端带宽优化
- 减少顶点数据:
- 移除不必要的UV或切线数据。
- 使用顶点压缩(如Unity的
MeshCompression
)。
- 实例化(GPU Instancing):
- 对重复物体(如草、树木)使用GPU Instancing,减少Draw Call。
- Early-Z测试:
- 在Shader中声明
ZTest LEqual
,避免不可见像素计算。
- 在Shader中声明
四、URP管线设置优化
- 启用SRP Batcher:
- 减少Draw Call和SetPass Call。
- 简化渲染特性:
- 禁用或简化阴影、反射探针、后处理效果。
- LOD分级:
#pragma shader_feature _LOW_DETAIL #if defined(_LOW_DETAIL) // 低细节Shader代码 #endif
五、复杂算法优化示例:移动端阴影
问题:逐像素阴影计算高开销。
优化方案:
- 使用预计算的阴影贴图(如烘焙静态阴影)。
- 简化阴影滤波(硬阴影代替软阴影)。
- 降低阴影分辨率:
c#
// URP Asset中设置阴影分辨率
ShadowCascadeSettings.shadowResolution = 512; // 从1024降低到512
六、调试工具
- Unity Profiler:
- 分析GPU时间,定位高开销Shader。
- RenderDoc:
- 捕获移动端帧数据,分析ALU指令和纹理采样。
- Adreno Profiler/Mali Graphics Debugger:
- 高通/ARM官方工具,直接分析Shader性能。
七、总结
- 核心原则:
- 极致简化数学计算:用近似代替精确,低精度代替高精度。
- 减少片段着色器负载:预计算、迁移到顶点着色器、LUT。
- 带宽敏感:合并纹理、压缩数据、减少采样。
- 典型优化场景:
- 将
pow(a, b)
替换为exp2(b * log2(a))
(某些GPU更快)。 - 用
mad
指令合并乘加运算。 - 对低端设备完全禁用复杂特效(如镜面反射)。
- 将
通过结合移动端GPU架构特性和URP的轻量化设计,可以显著提升移动端渲染性能,避免发热和卡顿。
八、整理出来的数学运算优化方案:
1. 快速倒数代替除法
// 常规除法
float depth = 1.0 / (far - near);
// 优化:使用rcp
float rcpDepth = rcp(far - near); // 生成1条ALU指令
float depth = rcpDepth;
2. 快速平方根倒数
// 常规计算
float len = 1.0 / sqrt(dot(v, v));
// 优化:使用rsqrt
float len = rsqrt(dot(v, v)); // 比sqrt+div快2-3倍
3. 泰勒展开近似三角函数
// 精确计算(高开销)
float y = sin(x);
// 泰勒3阶近似(误差<2%)
float y = x - x*x*x / 6.0; // 减少50% ALU
4. 光照模型简化:Lambert代替PBR
// 完整PBR(20+ ALU)
float D = GGX(n, h, roughness);
float G = Smith(n, v, l, roughness);
float F = FresnelSchlick(v, h, F0);
// 移动端简化(5 ALU)
half diffuse = saturate(dot(n, l));
half spec = pow(saturate(dot(v, reflect(l, n))), 32.0);
5. 菲涅尔效应近似
// 精确Schlick公式
float F = F0 + (1.0 - F0) * pow(1.0 - saturate(dot(v, h)), 5);
// Schlick简化版(省去pow)
float F = F0 + (1.0 - F0) * (1.0 - dot(v, h)) * 0.2;
6. 环境光遮蔽(AO)合并
// 分开计算
float ao = texture(aoMap, uv).r;
float shadow = texture(shadowMap, uv).r;
// 合并为单通道(RGBA分别存储不同数据)
float4 combined = texture(combinedMap, uv);
float ao = combined.r;
float shadow = combined.g;
7. 双边过滤替代高次采样 (纹理采样)
// 常规三线性采样
color = textureLod(tex, uv, 0);
// 双边快速近似(减少纹理读取)
float2 ddxUV = ddx(uv) * 0.5;
float2 ddyUV = ddy(uv) * 0.5;
color = (texture(tex, uv + ddxUV) + texture(tex, uv - ddxUV)
+ texture(tex, uv + ddyUV) + texture(tex, uv - ddyUV)) * 0.25;
8. Mipmap层级预计算
// 动态计算mip层级(高开销)
float mip = calcMipLevel(uv);
// 顶点着色器预计算
v2f vert() {
o.mip = log2(length(ddx(uv) + ddy(uv)));
}
// 片段着色器直接使用
color = textureLod(tex, uv, mip);
9. 符号函数代替if-else (分支消除)
用saturate()
替代范围判断
// 原始分支
if (x > 0.5) { y = 1; } else { y = 0; }
// 无分支实现
y = saturate(sign(x - 0.5) * 1000); // 利用saturate截断
用step()
替代if-else
// 原分支代码
if (uv.x > 0.5) {
color = red;
} else {
color = blue;
}
// 优化:step + lerp
float mask = step(0.5, uv.x);
color = lerp(blue, red, mask);
ALU节省:减少50%分支指令开销
向量化运算消除分支 (向量掩码混合)
// 原分支代码
if (isRed) {
color.r += 0.1;
} else {
color.b += 0.1;
}
// 优化:向量运算
float3 mask = float3(isRed, 0, !isRed);
color += 0.1 * mask; // 无分支
符号函数sign()
// 原分支代码
if (dir > 0) {
speed = 1.0;
} else {
speed = -1.0;
}
// 优化:sign函数
speed = sign(dir); // dir=0时需额外处理
预计算分支结果到纹理(LUT) ======》传说中的查表发 ,特好用
// 复杂分支逻辑
float GetTerrainType(float height) {
if (height < 0.3) return 0.0; // 水
else if (height < 0.6) return 0.5; // 草地
else return 1.0; // 岩石
}
// 优化:预存到1D纹理
float type = tex1D(_TerrainLUT, height).r;
位掩码存储多条件状态
// 8种状态用1个float存储(每位代表一个状态)
uint state = asuint(_Params.x);
bool isMoving = (state & 0x1) > 0; // 第1位
bool isVisible = (state & 0x2) > 0; // 第2位
手动展开短循环
// 原动态循环
for (int i = 0; i < loopCount; i++) {
// 计算...
}
// 优化:固定次数展开
#define LOOP_COUNT 4
for (int i = 0; i < LOOP_COUNT; i++) {
// 编译时展开(UNROLL指令)
}
#pragma unroll LOOP_COUNT
循环内计算外提
// 低效代码
for (int i = 0; i < 4; i++) {
if (useSpecular) {
spec += CalculateSpecular(i);
}
}
// 优化:分支外提
if (useSpecular) {
for (int i = 0; i < 4; i++) {
spec += CalculateSpecular(i);
}
}
变体:
条件编译剥离分支
#pragma shader_feature _USE_SHADOWS
// 运行时分支
#if defined(_USE_SHADOWS)
color *= CalculateShadow();
#endif
优化效果:完全消除未启用功能的代码
高级优化技巧===》概率性分支执行
// 高频细节处选择性跳过计算
float skipProb = frac(_Time.y * 0.1); // 时间驱动的随机
if (skipProb > 0.2) {
// 只执行80%的线程
color += DetailCalculation();
}
分支结果缓存
// 多次使用同一分支结果
float result = (condition) ? A : B;
color1 = result * 0.5;
color2 = result * 0.8;
平台特性适配
利用ARM Mali的branch_predication
// Mali GPU专用提示指令
#pragma arm Mali branch_predication on
if (condition) {
// 编译器优化分支预测
}
Adreno的[flatten]
属性
// 强制展开分支(Adreno SDK建议)
[flatten]
if (condition) {
// 代码块
}
分支优化效果对比表
优化方案 | ALU指令减少 | 适用场景 | 移动端推荐 |
---|---|---|---|
step()/saturate | 30-50% | 二选一颜色/数值混合 | ★★★★★ |
向量掩码混合 | 40-60% | 多条件状态控制 | ★★★★☆ |
LUT预计算 | 60-80% | 复杂分段函数 | ★★★★☆ |
循环展开 | 20-40% | 固定次数循环 | ★★★☆☆ |
Shader变体剥离 | 100% | 功能开关类分支 | ★★★★★ |
概率性执行 | 50-70% | 高频细节计算 | ★★☆☆☆ |
终极原则:
➤ 移动端尽量避免所有if-else
,用数学运算代替
➤ PC端可适度保留简单分支,但需确保同一Wave内条件一致
10. 法线压缩存储 (几何运算)
// 常规法线存储
float3 normal = texture(normalMap, uv).xyz * 2 - 1;
// 压缩为2通道(移动端常用)
float2 enc = texture(normalMap, uv).xy;
float3 normal = float3(enc, sqrt(1 - dot(enc, enc)));
11. 视差映射近似
// 精确视差偏移(高开销)
float2 offset = ParallaxOcclusionMapping(uv, viewDir);
// 快速浮雕映射(30%性能提升)
float2 offset = uv + viewDir.xy * height * 0.1;
12. sRGB线性化近似 (色彩空间优化)
// 精确sRGB->Linear
float3 linear = pow(srgb, 2.2);
// 快速近似(误差<3%)
float3 linear = srgb * (srgb * 0.305306011 + 0.682171111);
13. HDR压缩
// Reinhard色调映射
float3 mapped = hdr / (hdr + 1.0);
// 快速压缩(省去除法)
float3 mapped = hdr * exp(-hdr); // 适合低动态范围
14. 屏幕空间反射(SSR)降级 (高级)
// 完整步进追踪
for (int i=0; i<32; i++) { ... }
// 二分法快速近似(4次迭代)
float step = 0.5;
for (int i=0; i<4; i++) {
rayPos += rayDir * step;
step *= 0.5;
}
15. 体积光步进优化 (高级)
// 常规16次步进采样
for (int i=0; i<16; i++) { ... }
// 自适应步进(性能敏感区采样更密)
float step = i < 8 ? 0.1 : 0.2;
16. Early-Z预遮挡
// 手动触发Early-Z测试
[earlydepthstencil]
void frag() {
// 空颜色写入,仅深度测试
}
17. 深度预Pass
// 渲染队列拆分
Tags { "RenderType"="Opaque" "Queue"="Geometry-100" }
18. 乘加指令合并(硬件特性)
// 分离运算
float y = a * b + c; // 2条ALU
// 合并为mad指令(1条ALU)
float y = mad(a, b, c);
19. 向量化运算
// 标量计算
float r = a.x * b.x;
float g = a.y * b.y;
float b = a.z * b.z;
// 向量化计算(减少指令数)
float3 rgb = a.rgb * b.rgb;
优化原则总结:
优化类型 | ALU节省幅度 | 适用场景 |
---|---|---|
数学近似 | 30-50% | 光照、后处理 |
分支消除 | 10-20% | 条件逻辑 |
纹理采样合并 | 20-40% | PBR材质、多贴图对象 |
精度降级 | 15-30% | 移动端/低端GPU |
硬件指令优化 | 5-15% | 所有平台 |
实际项目中建议:
- 使用
Shader Variant
为不同设备提供不同精度的算法 - 通过
#pragma target 3.0
限制Shader模型版本强制简化 - 利用
UnityPerMaterial
CBUFFER合并材质参数
这些技巧配合RenderDoc、Mali GPU Analyzer等工具分析,可在保持视觉效果的前提下显著降低GPU负载。