Unity纹理的性能优化

https://developer.unity.cn/projects/6482ba86edbc2a116e4f27c1

在Unity的储存方式

大部分的纹理,Unity都会保存两份像素数据的副本:

  • GPU内存:对应的数据对象为RenderTexture,是渲染所需的数据
  • CPU内存:对应的数据对象为Texture,属于可选数据,又被成为可读纹理,用于读取/写入/控制像素数据

在Unity不同位置的像素数据交互方式

Unity纹理的性能优化_第1张图片
涉及到跨硬件环境的数据传输的API:

  • Texture2D.Apply
  • Texture2D.ReadPixels
  • Texture2D.Request

像素数据处理的场景选择

最常见的GPU像素数据处理操作,是在着色器里采集纹理样本;而在一些情况下,在CPU上调控纹理数据更合适,访问数据的方式会更灵活。

如何选择处理场景,可以参考以下问题:

  • GPU运算是否比CPU快?
    • 纹理缓存与采集的流程会造成什么样的压力?
      • 比如不使用mipmap采集高分辨率纹理很可能会减缓GPU速度
    • 流程是否需要随机写入纹理,或能否输出一份颜色或深度附件?
      • 写入纹理上的随机像素要求经常清理缓存,这会减缓整个流程
      • 纹理的随机访问在CPU上执行效率比GPU更高
  • 项目是否受GPU瓶颈限制?就算GPU执行进程的速度远快于CPU,它是否能继续承担更多作业且不超过帧预算?
    • 如果GPU和CPU主线程的帧耗时都接近了极限,那流程较慢的部分也许能由CPU工作线程执行
  • 有多少数据需要上传到GPU,或从GPU下载才能计算或处理结果?
    • 着色器或C#作业能否把数据打包成更小的格式来减少需要的带宽?
    • RenderTexture 能否精简采样成分辨率更低的纹理用于下载?
  • 流程能否批量执行?
    • 如果有大量数据需要同时处理,GPU有可能会内存不足
  • 处理结果是否紧急要用?运算或数据传输能否异步进行/随后处理?
    • 如果单张帧需要执行过多的工作,GPU可能会没有足够的时间来渲染实际的帧图像

纹理的可读与不可读

  • 默认导入项目的纹理是不可读的,从脚本上创建的纹理是可读的
  • 可读纹理占用的内存是不可读纹理的两倍
  • 不可读的纹理无法反向变回可读状态

Unity部分纹理API的耗时测试

1. CPU上的纹理像素复制

SetPixel

  • 说明:设置具体像素坐标(x,y)处的像素颜色
  • 耗时:≈1600ms
for(var y = 0; y < m_TextureSize, ++y)
	for(var x = 0; x < m_TextureSize, ++x)
		m_TargetTexture.SetPixel(x, y, m_SourceTexture.GetPixel(x,y));

m_TargetTexture.Apply();

SetPixels

  • 说明:设置整个像素颜色块
  • 耗时:≈35ms
m_TargetTexture.SetPixels(m_SourceTexture.GetPixels(0), 0);
m_TargetTexture.Apply();

SetPixels32

  • 说明:设置整个像素颜色块(接收Color32数组)
  • 耗时:≈5.7ms
m_TargetTexture.SetPixels32(m_SourceTexture.GetPixels32());
m_TargetTexture.Apply();

LoadRawTextureData

  • 说明:使用原始预格式化数据填充纹理像素
  • 耗时:
    • 直接GetRawTextureData():≈5.5ms
    • 直接GetRawTextureData():≈2.2ms
  • 其他说明:
    • GetRawTextureData()返回的是原始纹理数据
    • GetRawTextureData()返回的是原始纹理数据的内存副本,内部流程比 GetRawTextureData()多了复制操作,并且需要纹理是可读的
m_TargetTexture.LoadRawTextureData(m_SourceTexture.GetRawTextureData());
m_TargetTexture.Apply();

m_TargetTexture.LoadRawTextureData(m_SourceTexture.GetRawTextureData<byte>());
m_TargetTexture.Apply();

SetPixelData

  • 说明:用原始预格式化数据设置像素值
  • 耗时:≈2.2ms
m_TargetTexture.SetPixelData(m_SourceTexture.GetPixelData<byte>(0),0);
m_TargetTexture.Apply();

2. GPU上的纹理像素复制

CopyTexture

  • 说明:复制纹理内容
  • 耗时:
    • 不可读纹理:≈1.9ms
    • 可读纹理:≈2ms
Graphics.CopyTexture(m_SourceTexture, m_TargetTexture);

Blit

  • 说明:使用着色器将源纹理复制到目标渲染纹理
  • 耗时:≈1.5ms
Graphics.Blit(m_SourceTexture, m_TargetTexture);

3. CPU上的纹理像素更新(每帧)

SetPixel

  • 说明:设置具体像素坐标(x,y)处的像素颜色
  • 耗时:≈156.7ms
for(var y = 0; y < m_TextureSize, ++y)
	for(var x = 0; x < m_TextureSize, ++x)
		m_TargetTexture.SetPixel(x, y, ChangePixel());

SetPixels32

  • 说明:设置整个像素颜色块(接收Color32数组)
  • 耗时:≈133.3ms
Color[] m_Colors32 = new Color[m_TextureSize * m_TextureSize];
var idx = 0;

for(var y = 0; y < m_TextureSize, ++y)
	for(var x = 0; x < m_TextureSize, ++x)
		m_Colors32[idx++] = ChangePixel();
		
m_TargetTexture.SetPixel32(m_Colors32, 0);

SetPixels

  • 说明:设置整个像素颜色块
  • 耗时:≈104ms
Color[] m_Colors = new Color[m_TextureSize * m_TextureSize];
var idx = 0;

for(var y = 0; y < m_TextureSize, ++y)
	for(var x = 0; x < m_TextureSize, ++x)
		m_Colors [idx++] = ChangePixel();
		
m_TargetTexture.SetPixel(m_Colors , 0);

结论:

  • SetPixels32比SetPixels慢,原因是系统需要获取运算得出的Color浮点值,将其转换成基于字节的Color32结构

在Unity的纹理处理常用API

1. 推荐使用的API:CPU

  • CopyTexture:是将CPU像素数据转移到另一张纹理的最快方法,前提是两张纹理均可读;
  • GetPixelData、SetPixelData、GetRawTextureData、SetPixelData:SetPixelData可以将整个mip级别的数据复制到一张目标纹理上,GetPixelData所返回的Native Array会指向Unity内部CPU纹理数据的一个mip级别,实现不必复制任何像素就能直接读写数据

2. 推荐使用的API:GPU

  • CopyTexture:是将GPU像素数据转移到另一张纹理的最快方法,不会执行任何格式的转换;
  • Blit:可以用着色器把GPU数据快速转移到RenderTexture上,相比CopyTexture,会有一些与分辨率无关的启动成本,但是可执行的自定义操作性更高;

3. 谨慎使用的API

还有几种特殊方法可能会对性能产生重大影响,需要谨慎使用。

带底层数据转换的像素访问方法

GetPixel、GetPixelBilinear 、SetPixel 、GetPixels 、SetPixels、GetPixels32 、SetPixels32

这类方法能在不同程度上执行像素格式转换,其中Pixels32是里边性能最好的(但是如果纹理的底层格式不能完美匹配Color32结构,也会执行格式转换)。

在使用以上方法时,需要注意像素数量的增长

快速数据访问方法

GetRawTextureData 、LoadRawTextureData

这是两种只用于Texture2D的方法,可处理包含所有 mip 等级原始像素数据的数据组,并将 mip 按从大到小的顺序排序,每个 mip 带有“高度”数量的“宽度”像素值。

虽然能快速让 CPU 访问数据,但GetRawTextureData 有一个操作难点,就是不按模板写的派生方法会返回数据的副本。这种方式不仅更慢,还不能直接操纵Unity管理的底层缓冲区。GetPixelData没有这种特点,它只会返回指向底层缓冲区的NativeArray,该缓冲区在用户代码将控制权返回给 Unity 之前一直有效。

ConvertTexture

这是一种将纹理的 GPU 数据转移至另一张纹理的途径,源纹理和目标纹理不一定需要有同样的大小或格式。这种转换流程在各个情况下都会发挥最大效率,但是流程比较繁琐:

  • 分配一张匹配目标纹理的临时RenderTexture
  • 将源纹理Blit到临时的RenderTexture
  • 复制临时RenderTexture上的转移结果到目标纹理

是否需要使用该接口,可以根据以下问题:

  • 能否保证在导入时就以目标平台需要的大小/格式来创建源纹理?
  • 能否在流程中一直使用同一张纹理,把结果直接用作其他流程的输入?
  • 能否直接使用RenderTexture作为目标纹理?(这样转换流程仅需一次Blit)
ReadPixels

该函数会从激活的RenderTexture (RenderTexture.active) 同步下载 GPU 数据到 CPU 上的 Texture2D。可以用它来保存或处理某次渲染运算的输出。

从 GPU 下载数据是一个繁琐的流程。在下载开始前,ReadPixels 必须等待 GPU 完成之前的工作,并只会在请求的数据可用后返回,从而拖累性能。如果可以,使用AsynGPUReadback方法会更好。

一些建议

  • 操纵纹理时,首先要判断哪个环境下的运算能带来最优的性能,考虑因素包括CPU/GPU的工作负荷、输入/输出的数据带宽等;
  • 在一些特定的转换操作中,使用GetRawTextureData等低级函数,比一些复制并转换数据的高级函数更好更高效(尽管使用起来不够便捷);

你可能感兴趣的:(游戏开发,图形学/渲染,unity,性能优化,游戏引擎,纹理贴图)