渲染性能优化指南

前言

本篇介绍在 CocosCreator 环境下,对游戏程序渲染性能方面各种手段的考察。


# 一. 渲染性能

# 1.帧率:

是用户对程序的流畅程度比较直观的感受——如果游戏长时间处于一个低于设计帧率的状态,用户就会觉得“卡”。 从开发者的角度看,帧率降低,是缘于程序的各种运算和操作消耗了过大的系统资源,导致程序必须花费超过“预想的一帧时间”去完成它的工作。 所以一般来说,帧率不符合预期往往同时还伴随着设备发热,设备耗电加剧,设备内存占用过高导致崩溃等负面情况。

在一些应用场景中,游戏的帧率往往不仅受到绘制操作的影响。 其他方面的计算消耗,或者过大的内存占用,都会和帧率产生相互影响。

# 2.影响性能的情况:

以下是cocos游戏程序应用场景中,可能对整体性能有负面影响的情况:

  • 图形绘制部分造成了过大的开销 (比如最常见的就是Drawcalls 过高, 如何降低Drawcalls);
  • 非绘制部分的计算工作过于繁重 (比如每帧都会进行的大型for循环,数值计算,以碰撞检测和过于繁重的物理引擎计算最为常见);
  • 过高的内存占用和帧率的相互影响纹理压缩);
  1. 在浏览器环境下(h5,也包括微信小游戏),系统的内存回收是在程序的空闲时间见缝插针地执行。而过高的系统开销会导致内存收回找不到这样的空闲时间——也就是说,帧率降低的同时可能还存在内存垃圾无法回收的情况;

2) 因为自动垃圾回收机制的存在,过高的内存占用必然会导致内存回收耗费更长的时间。这个时间开销,也会反过来影响帧率。

图形绘制

“渲染性能”这个概念,其实还包含其他诸多方面。但接下来的内容将只局限于其中的一个范畴,即,图形绘制对游戏程序帧率的影响 。也就是说,只讨论上述情况中的第一种。


# 二. OpenGL 绘制命令

我们目前的、以及将来较长一段时间的游戏项目,都将会采用OpenGL或WebGL的渲染方式。

在代码层面,我们要完成一帧游戏画面的绘制,需要调用一系列的 GL绘制命令 ,其基本操作如下:

# 1. 清屏

将上一帧绘制的内容清空,一般如下:

glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

接着,按照层级关系,依次对显示元素进行接下来的2,3,4,5步骤:

# 2. 绑定纹理数据

首先是绑定纹理数据(亦称之为贴图,图片素材):

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture1);

# 3. 顶点数据

然后准备好顶点数据(包括顶点位置,索引,颜色),并绑定,比如:

    float vertices[] = {
        // positions          // colors           // texture coords
         0.5f,  0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f, ...
    };
    unsigned int indices[] = {
        0, 1, 3, // first triangle
        1, 2, 3  // second triangle
    };
    unsigned int VBO, EBO;

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);
    // texture coord attribute
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);

# 4. shader的加载

shader的加载,编译,使用:

//加载
...
// vertex shader 编译
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// fragment Shader编译
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
//创建shader实例,并绑定
ProgramID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram();
//赋值
glUniform1i(...);
//使用
glUseProgram(ProgramID);

# 5. 绘制顶点命令

准备工作就绪,调用绘制顶点命令

glDrawElements();
//或者
glDrawArrays();

# 6. 完成渲染

对所有显示元素均进行步骤2,3,4,5,直到遍历完整个渲染列表。


去掉详细代码的部分,绘制步骤精简为:

TIP

  1. 清屏。按层级关系依次对显示元素进行2,3,4,5步;
  2. 绑定纹理数据(亦称之为贴图,图片素材);
  3. 准备顶点数据(包括顶点位置,索引,颜色),并绑定;
  4. 编译使用shader;
  5. 调用 绘制顶点命令 glDrawElements()
  6. 对所有显示元素均进行步骤2,3,4,5。直到遍历完整个渲染列表。

以上即为我们的游戏程序调用OpenGL API去完成图形绘制的过程。 这个过程导致的系统开销,根本上决定了程序的渲染性能。而对这个过程的优化处理,即是对渲染性能的优化。


# 三. 导致帧率下降的因素

下面是一些常见的除drawcalls之外导致帧率下降的因素:

# 1. 绘制过多的顶点:

这种情况在3D游戏中更普遍——为了更细致的表现效果,3D模型的顶点数往往会非常多。 而2D游戏在大部分情况下,一个Sprite只有4个顶点 ,需要绘制的顶点数量通常比3D游戏更可控。

当然也有一些细节要注意:九宫(SLICED)拉伸模式会让Sprite的顶点数变成16个,TILED拉伸模式会让顶点数量不固定的变多,这取决于Sprite尺寸和原始图片尺寸的比例。还有文本框,如果文本框是用单个文字的纹理拼接的,那么一段内容比较大的文本框也会让顶点数暴增。

# 2. 用了算法复杂的 Shader:

比如在片元着色器中进行大型for循环和数值计算,这是非常考验设备性能的操作,表现效果在不同终端上往往会有非常大的差别。

这里还要提到顶点着色器和片元着色器的区别:

顶点着色器是对顶点进行逐一操作,而片元着色器是对光栅化后的像素进行逐一操作——通常情况下像素的数量是远多于顶点的。 比如对颜色进行相同的处理,(因为顶点的数量比像素少)用顶点着色器去完成会比片元着色器效率高得多,当然片元着色器能给出更好的表现效果(因为精确到像素)。这些问题是需要设计者权衡的。当然很多特效只能通过片元着色器才能完成,常见的比如Sprite模糊,Sprite描边。)

# 3. 绑定更多的纹理:

有一些特殊效果会需要我们绑定多个纹理给GPU,在 shader 中同时处理多个纹理数据。

OPENGL/WEBGL 默认可以同时在GPU绑定32个纹理数据,微信小游戏也支持同时绑定8个纹理数据。绑定和处理更多的纹理也会造成更多的系统开销。


# 四. 优化方案

性能方面:「发热、敌人数量增多时容易卡顿」

  • 发热是个比较综合的问题,一般来说 CPU 导致发热,降低 CPU 的工作会有效减少发热;
  • 卡顿则受到帧频和 drawcall 的影响比较大;

还有以下这些优化手段:

  • 降低帧数:目前已动态设置帧频,游戏过程 60 帧,非游戏过程 30 帧

  • 减少帧回调:目前 update 中还有很大的逻辑优化空间

  • 减少内存使用:这块目前也有很大的优化空间,GC 回收,节点池,对象和节点复用、缓存等等,甚至包括一些贴图的引用释放等

  • drawcall 优化:其实还可以借助一些帧调试工具去进一步分析,项目的后期应该还会对drawcall优化进行再深入一点的探索

有一些优化的大原则,在此补充一下,即不要过早优化、最好的优化就是不用优化等等。优化是没什么万金油的,每个项目的性能瓶颈都不一样,需要根据项目的实际瓶颈,判断是该优化内存还是加载时间还是渲染时间等等。