关于图片渲染的过程
挺久以前有个腾讯的面试, 也是基于游戏引擎开发东西的, 问到一个图片是怎样加载渲染的, 我最后说到压缩的图片直接进到GPU, 通过GPU解压获取最终像素, 然后被连着问了两次你确定?
然后我就有点迷糊了, 不是吗? 然后他也没说他为啥这样问, 或者想要的是什么答案, 就略了
现在回想起来, 可能是对于过程的理解或者平台的不同, 也会造成鸡同鸭讲的结果的吧...这里再把图片从创建资源到加载渲染的流程捋一遍.
最原始的位图文件BMP, 常见的压缩格式 JPG / PNG , 不常用格式 ETC / PVR 这些 :
它们都是同一张图片, 只是存的类型不一样, 位图文件就是RGB直接存的, 大小直接就能算出来 :
627 * 310 * 3Byte = 583,110 B => 571KB
可是相比压缩格式它占的空间实在太大了, 跟 JPG 差了几十倍, 不过 JPG 是有损压缩方式压缩的, 当解压回来后图像会损失信息(网络一些图片盗来盗去, 越来越绿就是这个原因), PNG 是无损压缩的, 解压回来还是那个样子, 所以根据不同情况基本这两种压缩就涵盖了有损和无损区别了.
而 ETC / PVR 这些也是有损压缩, 而它们的计算逻辑跟 JPG 也是不同的, ETC / PVR 对应于 GPU 硬件解压逻辑, 我们都知道GPU是并行单元的, 就像超市存包的锁柜, 每个柜子直接用钥匙打开就能取东西了, 可以同时 存/取 N个包, ECT / PVR 的解压是基于一个全局查找表逻辑的, 取出来的结果对照查找表然后进行调制就能得到像素值了:
BMP -> ETC / PVR
在内存上相当于缩小的BMP, 差不多除以6
定长压缩, 直接就能找到对应像素点的 Block, 因为 GPU 很猛, 解压计算很快, 就跟直接读取位图一样没有差别. 所以一般 ETC / PVR 图片还是保持压缩格式躺在 GPU 的内存里. 下面是非常直观的PVRTC还原像素过程 :
而 JPG / PNG 这些非定长压缩的, 你并不能直接从像素推断获取它对应像素点的数据在内存中的位置, GPU 就没有办法并行的去找内存然后解压对应像素信息了, 比如 JPG 自带了数据压缩 :
所以一个 JPG 的解压步骤很多, 把这些用 GPU 实现的话相当困难, 主要是算法复杂 :
上面说的是格式的压缩, 还有另外一个文件的压缩.
GPU 部分的差别大概就是这些了, 然后打包游戏资源的时候, 一般还会把这些压缩格式图片进行一次打包, 而这个打包也会进行数据压缩, 看看打包压缩对它们的影响 :
可以看到压缩对它们还是有很大影响的, JPG / PNG 自带了去冗余过程, 可压缩量不大, 甚至 PNG 反而文件变更大了, 而原始 BMP 文件可压缩量很可观, 当然 ETC / PVR 这些跟BMP 有点类似, 也是能压缩的 :
这是PC上的 DXT 压缩格式打的 AssetBundle 包, 它的 DXT 内存大小是显示在检视面板上的 :
而如果不压缩, 它在内存上的大小是这样的 :
PS : 这里为了符合GPU压缩格式, 打开 Power Of 2 了, 图片尺寸被限制到了 512x256 跟原始图片 627x310 大小有点差距, 不过不影响判断
这样看下来, 其实解压就有两种意思了, 一个是文件解压, 像上面的 zip 解压出图片文件, 然后是像素解压, 是把压缩格式的图片还原为像素的过程, 比如 NPG 文件还原为 BMP 格式然后传入 GPU, GPU 就躺着读取就行了, 还有就是 GPU 支持的压缩格式 DXT / ETC / PVR 等, 只需要把压缩格式图片传给GPU, 让 GPU 大大的干活就行了.
然后文件解压阶段, 必然是由 CPU 进行的, 比如加载 AssetBundle, 它就会在读取完文件之后进行解压操作(一般情况), 如果基于块压缩的(ChunkBasedCompression)它可能在读取相关资源的时候再对指定资源解压( AssetBundle.LoadAsset() ).
最后是 GPU 解压阶段 :
1. 如果 GPU 不支持你的压缩格式, 引擎会用 CPU 把图片解压成位图文件, 然后再传给 GPU, 这样又浪费 CPU 资源, 又浪费总线带宽
2. 如果支持的话, 就直接把压缩格式的图片传输给 GPU 了, 可以说是由 GPU 进行解压光栅化.
3. 复杂的压缩格式 JPG / PNG 基本不可能被 GPU 支持, 几乎都由 CPU 解压成位图传递给 GPU的
所以游戏引擎才会直接把扔进去的图片都进行对应平台的转换, 因为追求的是效率和压榨硬件性能, 而 Web 网页这种跨平台又是网络传输的, 一般都使用 JPG / PNG 这些自带压缩的格式, 虽然解压过程比较复杂. 下面是总结的流程图 :
左边就是通过 CPU 解压然后传递到 GPU 的过程, 可以看到它在系统中解压, 然后把非压缩的图片传递给了 GPU, 它的好处是任何平台都能通用, 并且可以使用算法复杂的压缩格式比如 JPG / PNG 等, 又或者当前压缩格式不是 GPU 能够解压的, 比如 PVRTC 格式放到安卓机子上, 它只能通过 CPU 解压之后传递位图给 GPU, 我们常说的 ETC2 格式在老的安卓机上会在 CPU 上解压成位图传递到 GPU 就是这个道理, 因为老的安卓机只能支持 ETC1, 然后 CPU 解压造成性能瓶颈.
右边就是非常理想的通过 GPU 解压的例子, 比如 ETC1 的图片跑在安卓机子上, PVR 的图片跑在 Meta 上, DXT5 的跑在 DX11 上, 既节省了传输的带宽, 又能利用 GPU 的并行解压, 压榨硬件性能.
这样回到最初的那个问答, 本地资源或是 AssetBundle 中读取出来的图片, 正常来说还是那样, 图片保持压缩格式进入 GPU 解压, 或者叫 Decode 得到像素值, 不过如果从 WWW 之类的获取到的 PNG / JPG 图片, 那就会在 CPU 中进行解压, 然后位图传入 GPU.
再加一个 WWW 下载来的图片跟本地图片加载后的差别吧, 上面的已经看到了本地图片内存占用 64KB(DXT), 而本地位图的话, 占570KB, 同样的图片放到服务器然后看看 :
public class Test : MonoBehaviour { public UnityEngine.UI.RawImage img; void Start() { StartCoroutine(Load(@"http://127.0.0.1:44599/StreamingAssets/PNG001.PNG")); } IEnumerator Load(string url) { var www = new WWW(url); yield return www; img.texture = www.texture; img.texture.name = "PNG001.PNG"; } }
变成2M了, 这是发布后的 exe 运行得到的数据.
按照理论计算这张图片的位图大小应该是 627x310x4 = 777KB (按照32位计算), 就算加上 Read/Write 属性, 也是 777 * 2 = 1.5MB, 不知道 Texture2D 的额外开销在哪, 反正是对 CPU 解压很不友好的了.
最近也看到了一些调用英伟达接口实现 JPEG 压缩/解压的, 比如这里提到的 : http://blog.csdn.net/weixinhum/article/details/46683509 ( CUDA 实现JPEG图像解码为RGB数据 )
不过这些都不是业界标准接口, 没有太多意义, 并且不是在 GPU 上实现的, 始终只是通过 GPU 提供辅助 压缩/解压 服务给 CPU 调用, 最终还是走的 CPU 解压的路子, 不过反过来看, 它可以给视频流提供 GPU 的解压辅助, 对各种需要高速解压识别的图像应该有惊喜...