UPL9-10——减少 Shader 采样开销
UPL9-10——减少 Shader 采样开销
减少纹理采样
纹理采样是所有内存带宽开销的核心消耗
- 使用的纹理越少、分辨率越合适,缓存命中率更高,带宽占用更低,性能更好
- 使用的纹理越多,则在调用过程中缓存丢失的情况就越多
- 制作的纹理越大,将它们传输到纹理缓冲消耗的内存带宽就越多
因此我们应该尽量的杜绝这些情况的发生,避免产生 GPU 瓶颈
比如我们可以通过把多个纹理合并成一张纹理的方式,
有效的减少纹理采样:可以把 金属度、粗糙度、环境光遮蔽 三张黑白图 打包到一张 RGBA 纹理的 RGB 通道中
使用更少的纹理数据
每次采样传输的数据量越小,显存带宽和缓存压力就越小
我们可以通过降低分辨率、减少通道的方式,去掉用不到的高精度
比如我们可以用 R8 格式代替 RGBA32 格式
遮罩贴图只需要一个灰度通道,用 R8 格式即可
-
R8:
每个像素只存储一个通道(R 通道)
每个像素 8 位 = 1 字节 -
RGBA32:
每个像素存储四个通道(RGBA 通道)
每个像素 32 位 = 4 字节
测试不同 GPU 纹理压缩格式
Unity 中不同的纹理压缩格式,能减少程序的磁盘空间以及运行时 CPU、内存使用率
这些压缩格式都是为具体平台的 GPU 架构设计的,压缩格式有很多,比如:DXT、PVRTC、ETC、ETC2、ASTC 等等

但是在不同的平台上能选择的压缩格式有所不同,比如:
- PC:BC 系列
- Android:ETC2 / ASTC
- iOS:PVRTC / ASTC
我们应该在不同平台测试不同格式,选出兼顾视觉和性能表现的格式进行使用
虽然 Unity 在不同平台都推荐了对应的格式,但是我们完全可以根据自己的需求
局部性的修改部分纹理的压缩格式,能带来更多的性能提升!
Unity 压缩格式说明:https://docs.unity3d.com/cn/current/Manual/class-TextureImporterOverride.html
最小化纹理交换
如果内存带宽存在问题,就需要减少正在进行的纹理采样量
并且频繁绑定不同纹理会刷新 GPU 的纹理缓存,导致 缓存丢失,更耗带宽
我们可以通过以下方式降低纹理采样量和交换频率
- 降低纹理分辨率(可能会牺牲表现效果,谨慎使用)
- 重复使用纹理(利用不同着色器渲染不同效果)
- 纹理合并图集(减少纹理交换次数)
- 减少在一个 Pass 或者 DrawCall 间频繁切换大纹理
等等
VRAM(显存)限制
显存不足会导致驱动把纹理换入换出(释放显存中老纹理,载入新纹理),会造成采样时卡顿,即便没有内存溢出,大纹理也会拖慢显存带宽
我们应该控制项目的显存预算,避免大纹理和冗余贴图的使用
比如:移动端项目尽量把总贴图内存控制在 500MB 以下
使用 Mipmap 并正确过滤
对于离摄像机很远的对象,如果一直使用大分辨率纹理,会带来多余带宽消耗,甚至带来画面闪烁
我们可以通过开启 Mipmap,在采样时使用线性(linear)或者三线性(trilinear)
这样不仅可以降低显存的带宽消耗,还可以减少锯齿,提升画质
为何会画面闪烁:
当物体离得很远时,一个屏幕像素可能对应 很多个纹理像素,如果没有 Mipmap,GPU 仍然从 最高分辨率的原始贴图里采样
就可能出现 在像素点和纹理点之间采样跳来跳去,会出现 锯齿、闪烁、莫尔条纹,
Mipmap 实际上起到了 低通滤波 的作用,去掉了超过屏幕采样率的高频细节
莫尔条纹:当两个 高频的规则图案(例如网格、条纹、点阵)叠加时,因采样不足或频率接近而产生的 干涉花纹
比如:
- 电视、电脑屏幕中出现的波浪纹
- 在游戏里看远处的格子地砖、栅栏,会出现水波纹状的图案
采样复用
建议一次纹理采样的结果尽量多次使用,而不是重复采样
或者把多个数据打包到同一张贴图中,一次采样取多种信息(知识点一中的减少纹理采样方式)
我们每次从纹理中采样都可能走一个完整的流程:显存 → 缓存 → 纹理单元 → 插值滤波
即使从缓存中获取内容也是有开销的,因此我们应该避免重复采样
比如:
1 | float4 albedo1 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv); |
应该改为
1 | float4 tex = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv); |
避免动态分支选择纹理
建议不要在 if/else 分支语句中进行采样,比如:
1 | if (条件) |
因为由于 GPU 的并行架构,动态分支可能会导致两个分支都执行,就会造成两个纹理都采样,反而会更慢
动态分支选择纹理 ≈ GPU 并不会真的只执行一条分支,所以会浪费采样
我们应该通过
-
纹理图集
把多张小贴图打到一张大图里,CPU 侧把本次要用的子矩形转换成 scale、offset 传给材质
着色器直接用重映射后的 UV 采样 同一张纹理 -
Texture2DArray把同尺寸、同格式的多张纹理打包成
Texture2DArray,着色器用float3(uv, layerIndex)采样 -
关键字产生变体
利用 Shader 中的关键字生成 Shader 变体,编译两个版本,运行时根据关键字选择执行的 Shader 版本
具体可见:US5L3——Shader变体和关键字
其中,2 和 3 是为了解决并行性的问题,和 1 的原理不一样
等等
压缩法线纹理
法线贴图通常是 RGB 或 RGBA32,每像素 24 位 3 字节 或 32 位 4 字节,开销大
但是法线数据的特点是:只需要保持方向向量的精度,不需要颜色准确
所以可以用专门的纹理压缩格式来大幅减少显存占用和带宽
基本原理:法线其实是一个三维向量 (x,y,z),但可以只存两个分量 (x,y)
再在 Shader 里用公式重建,公式 n.z = sqrt(saturate(1 - n.x * n.x - n.y * n.y));
这样可以用 RG 两通道存储,而不是 RGB(A)
Unity 内的专用压缩格式:
- BC5、ATI2:两个通道独立压缩,精度足够恢复法线方向,常用,主要针对 PC 和主机
- ASTC、ETC2_RG:支持更灵活的压缩方式,能兼顾效果和性能,主要针对移动端
等等
通过压缩法线贴图可以减少 50%~75% 的存储和带宽
流式加载
对于一些大型游戏来说,单个场景中可能会有几个 GB 的贴图
如果我们一次性的全部加载进显存,可能会造成卡顿甚至内存溢出直接闪退
而流式加载就是来解决这个问题的
-
虚拟纹理
将超大纹理分割成很多小块,运行时只加载摄像机能看到的块
详细可见:UPL2-13——Virtual Texturing 模块
-
Unity 自带的 Streaming Mipmaps 功能
GPU 只保留必要的 mip 层,远处加载低分辨率 mip,近处加载高分辨率 mip
并且这些流式加载功能往往是通过异步加载的方式进行的,这样可以避免主线程卡顿

