使用 Unity 开发 Android 游戏时如何追踪性能问题

图片 16

前言

两周前我开始用 Unity 开发一个叫 SkyBlocks 的 Android 游戏。游戏已经在
Google Play 上架了,如果你有时间可以下载来玩一玩儿。

开发的过程中遇到的最大的问题就是性能问题。我开始慢慢尝试分析到底是什么导致的性能问题以及我该怎么解决它。

版本记录

版本号 时间
V1.0 2017.10.01

版本记录

版本号 时间
V1.0 2018.01.17

Sky Blocks 游戏机制

这个游戏(SkyBlocks)有点像倒过来的俄罗斯方块和太空入侵者的合体。游戏的玩法就是把方块摆成一行,这是这行方块就会移到游戏面板的最上方。但是这行方块不会像俄罗斯方块那样完全消失。你有60秒的时候来摆出行数尽可能多的方块。UFO
会入侵“地面”(游戏面板的下方),还会极力破坏你建好的一切东西。一旦它们穿过你的防御,就开始破坏地球,当地球血量到
0 的时候,游戏就结束了。
听起来简单,做出来难,但是非常有意思,有趣极了!

前言

OpenGL
ES是一个强大的图形库,是跨平台的图形API,属于OpenGL的一个简化版本。iOS系统可以利用OpenGL
ES将图像数据直接送入到GPU进行渲染,这样避免了从CPU进行计算再送到显卡渲染带来的性能的高消耗,能带来来更好的视频效果和用户体验。接下来几篇就介绍下iOS
系统的 OpenGL ES框架。感兴趣的可以看上面几篇。
1. OpenGL ES 框架详细解析(一) ——
基本概览
2. OpenGL ES 框架详细解析(二) —— 关于OpenGL
ES
3. OpenGL ES 框架详细解析(三) —— 构建用于iOS的OpenGL
ES应用程序的清单
4. OpenGL ES 框架详细解析(四) —— 配置OpenGL
ES的上下文
5. OpenGL ES 框架详细解析(五) —— 使用OpenGL
ES和GLKit进行绘制
6. OpenGL ES 框架详细解析(六) ——
绘制到其他渲染目的地
7. OpenGL ES 框架详细解析(七) ——
多任务,高分辨率和其他iOS功能
8. OpenGL ES 框架详细解析(八) —— OpenGL ES
设计指南

前言

OpenGL
图形库项目中一直也没用过,最近也想学着使用这个图形库,感觉还是很有意思,也就自然想着好好的总结一下,希望对大家能有所帮助。下面内容来自欢迎来到OpenGL的世界。
1. OpenGL 图形库使用(一) ——
概念基础
2. OpenGL 图形库使用(二) ——
渲染模式、对象、扩展和状态机
3. OpenGL 图形库使用(三) ——
着色器、数据类型与输入输出
4. OpenGL 图形库使用(四) ——
Uniform及更多属性
5. OpenGL 图形库使用(五) ——
纹理
6. OpenGL 图形库使用(六) ——
变换
7. OpenGL 图形库的使用(七)——
坐标系统之五种不同的坐标系统(一)
8. OpenGL 图形库的使用(八)——
坐标系统之3D效果(二)
9. OpenGL 图形库的使用(九)——
摄像机(一)
10. OpenGL 图形库的使用(十)——
摄像机(二)
11. OpenGL 图形库的使用(十一)——
光照之颜色
12. OpenGL 图形库的使用(十二)——
光照之基础光照
13. OpenGL 图形库的使用(十三)——
光照之材质
14. OpenGL 图形库的使用(十四)——
光照之光照贴图
15. OpenGL 图形库的使用(十五)——
光照之投光物
16. OpenGL 图形库的使用(十六)——
光照之多光源
17. OpenGL 图形库的使用(十七)——
光照之复习总结
18. OpenGL 图形库的使用(十八)——
模型加载之Assimp
19. OpenGL 图形库的使用(十九)——
模型加载之网格
20. OpenGL 图形库的使用(二十)——
模型加载之模型
21. OpenGL 图形库的使用(二十一)——
高级OpenGL之深度测试
22. OpenGL 图形库的使用(二十二)—— 高级OpenGL之模板测试Stencil
testing
23. OpenGL 图形库的使用(二十三)——
高级OpenGL之混合Blending
24. OpenGL 图形库的使用(二十四)—— 高级OpenGL之面剔除Face
culling
25. OpenGL 图形库的使用(二十五)——
高级OpenGL之帧缓冲Framebuffers
26. OpenGL 图形库的使用(二十六)——
高级OpenGL之立方体贴图Cubemaps
27. OpenGL 图形库的使用(二十七)—— 高级OpenGL之高级数据Advanced
Data
28. OpenGL 图形库的使用(二十八)—— 高级OpenGL之高级GLSL Advanced
GLSL
29. OpenGL 图形库的使用(二十九)—— 高级OpenGL之几何着色器Geometry
Shader

不要忘记做设计

重要的事情是永远不要忘记先做设计。当我开始开发 SkyBlocks
是我也不知道我想要做什么,我更不知道这个游戏应该是个什么样子,
但是我从没想过该怎么去处理这个问题。幸好我以前用 JavaScript 和 HTML5
做过俄罗斯方块,我仅仅通过复制粘贴,并且修改了一些小
BUG,像旋转时的碰撞检测的方式把这些写过的代码移植到 C# 而没有考虑从 2D
到 3D 的区别。

自从在每次更新的时候,我不再一次性的绘制整个游戏面板,我就不得不创建每一行到一个
GameObject
中,并且用创建的简单的立方体渲染在网格中已经被锁定的块。网格每次更新的时候我都得销毁所有的块,并且又从新创建这些块。对于我来说,我觉得这已经够好了,游戏在电脑上运行的还挺好的。

然而,我没考虑到的是,游戏面板(网格)有 10 行 20 列,这有可能要有 200
个立方体不停地渲染,销毁,重建。这还不是最坏的,如果有必要的话行数会变得更多。并且每个立方体也有它自己的引用资源这使得每个立方体都会调用一次绘图。
想象一下,铺满一个游戏面板大约需要 150 到 200
个块被渲染。这就需要大约调用 200 次绘图。

如果在我移植代码之前做了设计,我就知道这个游戏不能长时间的运行。如果在开始动手之前有这就有想法,我就不会浪费那么多时间了。

Tuning Your OpenGL ES App – 调整您的OpenGL ES应用程序

iOS中OpenGL ES应用程序的性能与OS X或其他桌面操作系统中OpenGL的性能不同。
虽然功能强大的计算设备,基于iOS设备的桌面或笔记本电脑不具备内存或CPU功能。
嵌入式GPU通过与典型的台式机或笔记本电脑GPU可能使用的算法不同,优化了较低的内存和功耗。
低效渲染图形数据可能会导致较差的帧速率,或显着降低基于iOS设备的电池寿命。

后面的章节介绍了许多提高应用程序性能的技术; 本章涵盖整体策略。
除非另有说明,否则本章中的建议涉及OpenGL ES的所有版本。


实例化

假设你有一个绘制了很多模型的场景,而大部分的模型包含的是同一组顶点数据,只不过进行的是不同的世界空间变换。想象一个充满草的场景:每根草都是一个包含几个三角形的小模型。你可能会需要绘制很多根草,最终在每帧中你可能会需要渲染上千或者上万根草。因为每一根草仅仅是由几个三角形构成,渲染几乎是瞬间完成的,但上千个渲染函数调用却会极大地影响性能。

如果我们需要渲染大量物体时,代码看起来会像这样:

for(unsigned int i = 0; i < amount_of_models_to_draw; i++)
{
    DoSomePreparations(); // 绑定VAO,绑定纹理,设置uniform等
    glDrawArrays(GL_TRIANGLES, 0, amount_of_vertices);
}

如果像这样绘制模型的大量实例(Instance),你很快就会因为绘制调用过多而达到性能瓶颈。与绘制顶点本身相比,使用glDrawArraysglDrawElements函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU
to GPU Bus)上进行的)。所以,即便渲染顶点非常快,命令GPU去渲染却未必。

如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)

实例化这项技术能够让我们使用一个渲染调用来绘制多个物体,来节省每次绘制物体时CPU
->
GPU的通信,它只需要一次即可。如果想使用实例化渲染,我们只需要将glDrawArraysglDrawElements的渲染调用分别改为glDrawArraysInstancedglDrawElementsInstanced就可以了。这些渲染函数的实例化版本需要一个额外的参数,叫做实例数量(Instance
Count),它能够设置我们需要渲染的实例个数。这样我们只需要将必须的数据发送到GPU一次,然后使用一次函数调用告诉GPU它应该如何绘制这些实例。GPU将会直接渲染这些实例,而不用不断地与CPU进行通信。

这个函数本身并没有什么用。渲染同一个物体一千次对我们并没有什么用处,每个物体都是完全相同的,而且还在同一个位置。我们只能看见一个物体!处于这个原因,GLSL在顶点着色器中嵌入了另一个内建变量,gl_InstanceID

在使用实例化渲染调用时,gl_InstanceID会从0开始,在每个实例被渲染时递增1。比如说,我们正在渲染第43个实例,那么顶点着色器中它的gl_InstanceID将会是42。因为每个实例都有唯一的ID,我们可以建立一个数组,将ID与位置值对应起来,将每个实例放置在世界的不同位置。

为了体验一下实例化绘制,我们将会在标准化设备坐标系中使用一个渲染调用,绘制100个2D四边形。我们会索引一个包含100个偏移向量的uniform数组,将偏移值加到每个实例化的四边形上。最终的结果是一个排列整齐的四边形网格:

图片 1

每个四边形由2个三角形所组成,一共有6个顶点。每个顶点包含一个2D的标准化设备坐标位置向量和一个颜色向量。
下面就是这个例子使用的顶点数据,为了大量填充屏幕,每个三角形都很小:

float quadVertices[] = {
    // 位置          // 颜色
    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
    -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,

    -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
     0.05f, -0.05f,  0.0f, 1.0f, 0.0f,   
     0.05f,  0.05f,  0.0f, 1.0f, 1.0f                   
};  

片段着色器会从顶点着色器接受颜色向量,并将其设置为它的颜色输出,来实现四边形的颜色:

#version 330 core
out vec4 FragColor;

in vec3 fColor;

void main()
{
    FragColor = vec4(fColor, 1.0);
}

到现在都没有什么新内容,但从顶点着色器开始就变得很有趣了:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
    vec2 offset = offsets[gl_InstanceID];
    gl_Position = vec4(aPos + offset, 0.0, 1.0);
    fColor = aColor;
}

这里我们定义了一个叫做offsets的数组,它包含100个偏移向量。在顶点着色器中,我们会使用gl_InstanceID来索引offsets数组,获取每个实例的偏移向量。如果我们要实例化绘制100个四边形,仅使用这个顶点着色器我们就能得到100个位于不同位置的四边形。

当前,我们仍要设置这些偏移位置,我们会在进入渲染循环之前使用一个嵌套for循环计算:

glm::vec2 translations[100];
int index = 0;
float offset = 0.1f;
for(int y = -10; y < 10; y += 2)
{
    for(int x = -10; x < 10; x += 2)
    {
        glm::vec2 translation;
        translation.x = (float)x / 10.0f + offset;
        translation.y = (float)y / 10.0f + offset;
        translations[index++] = translation;
    }
}

这里,我们创建100个位移向量,表示10×10网格上的所有位置。除了生成translations数组之外,我们还需要将数据转移到顶点着色器的uniform数组中:

shader.use();
for(unsigned int i = 0; i < 100; i++)
{
    stringstream ss;
    string index;
    ss << i; 
    index = ss.str(); 
    shader.setVec2(("offsets[" + index + "]").c_str(), translations[i]);
}

在这一段代码中,我们将for循环的计数器i转换为一个string,我们可以用它来动态创建位置值的字符串,用于uniform位置值的索引。接下来,我们会对offsets
uniform数组中的每一项设置对应的位移向量。

现在所有的准备工作都做完了,我们可以开始渲染四边形了。对于实例化渲染,我们使用glDrawArraysInstancedglDrawElementsInstanced。因为我们使用的不是索引缓冲,我们会调用glDrawArrays版本的函数:

glBindVertexArray(quadVAO);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);

glDrawArraysInstanced的参数和glDrawArrays完全一样,除了最后多了个参数用来设置需要绘制的实例数量。因为我们想要在10×10网格中显示100个四边形,我们将它设置为100.运行代码之后,你应该能得到熟悉的100个五彩的四边形。


解决问题

解决问题的最好的办法是先勾勒出你的想法,然后再逐步深入。怎样让各个部分结合起来像一个整体一样的工作?这些不同的部分都做些什么?在
Sky Blocks 项目里,部分指的是游戏面板,防御线和 UFO。

游戏面板仅仅是为了控制游戏的,在游戏面板上有移动的块和已经被锁定的静止的块。而防御线仅仅是那10个立方体的静止的线。UFO
是一个可以移动到防御线上方的组合的网格。而抓住这些部分我就接近胜利了。

Debug and Profile Your App with Xcode and Instruments – 使用Xcode和仪器调试和配置您的应用程序

在各种设备上的各种场景中测试其性能之前,请勿优化应用程序。
XcodeInstruments包括帮助您确定应用程序中的性能和正确性问题的工具。

  • 监视Xcode调试量表,以了解性能的一般概述。
    当您从Xcode运行应用程序时,可以看到这些仪表,以便在开发应用程序时轻松发现性能变化。
  • 使用仪器中的OpenGL ES分析和OpenGL
    ES驱动程序工具,以更深入地了解运行时性能。
    获取关于您的应用程序的资源使用和符合OpenGL
    ES最佳做法的详细信息,并选择性地禁用部分图形管道,以便您可以确定哪个部分是您的应用程序中的重大瓶颈。
    有关更多信息,请参阅 Instruments User
    Guide。
  • 在Xcode中使用OpenGL ES Frame
    Debugger和性能分析器工具来精确定位性能和渲染问题。
    捕获用于渲染和呈现单个帧的所有OpenGL
    ES命令,然后遍历这些命令,以查看每个对OpenGL
    ES状态,绑定的资源和输出帧缓冲区的影响。
    您还可以查看着色器源代码,编辑它,并查看更改如何影响渲染的图像。
    在支持OpenGL ES 3.0的设备上,Frame
    Debugger还指出哪些绘图调用和着色器指令对渲染时间最有贡献。
    有关这些工具的更多信息,请参阅Xcode OpenGL ES Tools
    Overview。

实例化数组

虽然之前的实现在目前的情况下能够正常工作,但是如果我们要渲染远超过100个实例的时候(这其实非常普遍),我们最终会超过最大能够发送至着色器的uniform数据大小上限。它的一个代替方案是实例化数组(Instanced Array),它被定义为一个顶点属性(能够让我们储存更多的数据),仅在顶点着色器渲染一个新的实例时才会更新。

使用顶点属性时,顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。而当我们将顶点属性定义为一个实例化数组时,顶点着色器就只需要对每个实例,而不是每个顶点,更新顶点属性的内容了。这允许我们对逐顶点的数据使用普通的顶点属性,而对逐实例的数据使用实例化数组。

为了给你一个实例化数组的例子,我们将使用之前的例子,并将偏移量uniform数组设置为一个实例化数组。我们需要在顶点着色器中再添加一个顶点属性:

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aOffset;

out vec3 fColor;

void main()
{
    gl_Position = vec4(aPos + aOffset, 0.0, 1.0);
    fColor = aColor;
}

我们不再使用gl_InstanceID,现在不需要索引一个uniform数组就能够直接使用offset属性了。

因为实例化数组和position与color变量一样,都是顶点属性,我们还需要将它的内容存在顶点缓冲对象中,并且配置它的属性指针。我们首先将(上一部分的)translations数组存到一个新的缓冲对象中:

unsigned int instanceVBO;
glGenBuffers(1, &instanceVBO);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, 0);

之后我们还需要设置它的顶点属性指针,并启用顶点属性:

glEnableVertexAttribArray(2);
glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
glBindBuffer(GL_ARRAY_BUFFER, 0);   
glVertexAttribDivisor(2, 1);

这段代码很有意思的地方在于最后一行,我们调用了glVertexAttribDivisor。这个函数告诉了OpenGL该什么时候更新顶点属性的内容至新一组数据。它的第一个参数是需要的顶点属性,第二个参数是属性除数(Attribute
Divisor)。默认情况下,属性除数是0,告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。将它设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。而设置为2时,我们希望每2个实例更新一次属性,以此类推。我们将属性除数设置为1,是在告诉OpenGL,处于位置值2的顶点属性是一个实例化数组。

如果我们现在使用glDrawArraysInstanced,再次渲染四边形,会得到以下输出:

图片 2

这和之前的例子是完全一样的,但这次是使用实例化数组实现的,这让我们能够传递更多的数据到顶点着色器(只要内存允许)来用于实例化绘制。

为了更有趣一点,我们也可以使用gl_InstanceID,从右上到左下逐渐缩小四边形:

void main()
{
    vec2 pos = aPos * (gl_InstanceID / 100.0);
    gl_Position = vec4(pos + aOffset, 0.0, 1.0);
    fColor = aColor;
}

结果就是,第一个四边形的实例会非常小,随着绘制实例的增加,gl_InstanceID会越来越接近100,四边形也就越来越接近原始大小。像这样将实例化数组与gl_InstanceID结合使用是完全可行的。

图片 3

如果你还是不确定实例化渲染是如何工作的,或者想看看所有代码是如何组合起来的,你可以在这里找到程序的源代码。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <learnopengl/shader.h>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // ---------------
    Shader shader("10.1.instancing.vs", "10.1.instancing.fs");
 // generate a list of 100 quad locations/translation-vectors
    // ---------------------------------------------------------
    glm::vec2 translations[100];
    int index = 0;
    float offset = 0.1f;
    for (int y = -10; y < 10; y += 2)
    {
        for (int x = -10; x < 10; x += 2)
        {
            glm::vec2 translation;
            translation.x = (float)x / 10.0f + offset;
            translation.y = (float)y / 10.0f + offset;
            translations[index++] = translation;
        }
    }

    // store instance data in an array buffer
    // --------------------------------------
    unsigned int instanceVBO;
    glGenBuffers(1, &instanceVBO);
    glBindBuffer(GL_ARRAY_BUFFER, instanceVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(glm::vec2) * 100, &translations[0], GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // ------------------------------------------------------------------
    float quadVertices[] = {
        // positions     // colors
        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
         0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
        -0.05f, -0.05f,  0.0f, 0.0f, 1.0f,

        -0.05f,  0.05f,  1.0f, 0.0f, 0.0f,
         0.05f, -0.05f,  0.0f, 1.0f, 0.0f,
         0.05f,  0.05f,  0.0f, 1.0f, 1.0f
    };
    unsigned int quadVAO, quadVBO;
    glGenVertexArrays(1, &quadVAO);
    glGenBuffers(1, &quadVBO);
    glBindVertexArray(quadVAO);
    glBindBuffer(GL_ARRAY_BUFFER, quadVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
    // also set instance data
    glEnableVertexAttribArray(2);
    glBindBuffer(GL_ARRAY_BUFFER, instanceVBO); // this attribute comes from a different vertex buffer
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glVertexAttribDivisor(2, 1); // tell OpenGL this is an instanced vertex attribute.


    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // draw 100 instanced quads
        shader.use();
        glBindVertexArray(quadVAO);
        glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100); // 100 triangles of 6 vertices each
        glBindVertexArray(0);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    glDeleteVertexArrays(1, &quadVAO);
    glDeleteBuffers(1, &quadVBO);

    glfwTerminate();
    return 0;
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

虽然很有趣,但是这些例子并不是实例化的好例子。是的,它们的确让你知道实例化是怎么工作的,但是我们还没接触到它最有用的一点,绘制巨大数量的相似物体。出于这个原因,我们将会在下一部分进入太空探险,见识实例化渲染真正的威力。

这些例子不是实例的好例子,不过挺有意思的。它们可以让你对实例的工作方式有一个概括的理解,但是当绘制拥有极大数量的相同物体的时候,它极其有用,现在我们还没有展示呢。出于这个原因,我们将在接下来的部分进入太空来看看实例渲染的威力。


减少绘图调用次数

我在前面已经提到过,在这个游戏里我调用了太多次的绘图,我在我的 Android
机器上(a samsung galaxy
s4)上测试我的游戏,随着功能的完善,我发现我的游戏运行的越来越慢,跟蜗牛一样。

为了让游戏运行的更好,减少绘图调用是一项重要的任务。我不得不在网上找答案。绘图调用消耗多少性能?是什么引起了绘图调用?怎样才能减少调用?

在性能的提升上,我设计不出一个好的实验方案,但是我找到了一个可以在 CPU
和 GPU
上都能运行的方案。虽然一些实验中性能上没有明显的区别,但是在我将游戏绑定到
FPS 上的大部分的实验会让游戏运行的缓慢一些

主要的原因是很容易发现的,是由于所有的立方体被分开使用素材渲染。

为了在游戏面板上减少绘图调用,我决定减少对象的数量和不同种素材的数量。

因此我试着实现了一个功能,这个功能在游戏面板上替换了以前可能分开的绘制200个立方体,而现在只需绘制一整块网格。然后我选择用顶点颜色替换了单色纹理。并且将素材着色器改变成我在网上找的无光源顶点颜色。现在我把游戏面板上多次的绘图调用变成仅仅一次。

对于防御线,我做了类似的事情,我把所有的素材修改成相同的无光源顶点颜色,但是我没有让它们作为一个网格来渲染,我沿用以前的每
10
个立方体一个防御线的策略,这是因为我已经把素材变成共享的了,这就可以把之前多次防御线的绘图调用变成一次调用。

不幸的是,由于我没对调整之前的游戏截屏,但是我又实在不想调整成以前的解决方案,因此不能向各位展示调整前后的区别。

下面这张图片是优化后的截屏,但它也只能是张图,你可以通过这张图片了解我的游戏。

图片 4活动块调用了一次绘图指令,参考(Batches:
20 或者 SetPass calls)。就像我前面所说的,以前每个块包包含 4-5
个独立的立方体并且每个都含有素材引用。因此正如你看到的每个块本身都要通过至少四次才能创建出来。

图片 5而现在在顶端的两个被锁定的块,我们仅仅使用了一次额外的绘图调用。而这些块还是活动块时,
都是由原始的立方体组成并且每个立方体至少要经过一次处理才能创建出来。

图片 6防御线使用相同的模式,但是这里只有
10
个原始的使用顶点颜色和共享素材的四方体。实际上,在这里我们不需要在一个网格里绘制完整的防御线,Unity
帮我们自动的将完整的防御线添加到“通过批处理保存”。

UFO 比较灵活些。每个 UFO 被分成 3 个独立的网格: 上,中,下。

由于我想随机的出现 UFO,并且随机的让 UFO 一部分活动 。 因此每个 UFO
的每个部分有3-4个素材。一个 UFO 大概有 12-17
次的绘图调用,然而我却发现每个 UFO 实际上有 17-30
次的绘图调用。而我大概会有 2-3 个 UFO 几乎同时出现在屏幕上,因此就有大概
50-100 次的绘图调用。好疼啊!

图片 7

而在此刻,我非常非常渴望我能减少任何我能减少的绘图调用。因此我在网上找到了一个可以将所有网格合并成一个的脚本。但是这个这个脚本不能真正的适当的处理素材,所以我只能使用一种颜色的
UFO。我只能放弃多彩的漂亮的 UFO,而选用单调的讨厌的单一颜色的
UFO。通过对这个脚本的调整,我可以使用至少 2 种不同的颜色和一个纹理。
值得吗?当然了。30
次绘图调用听起来很多,也确实是。但是它表现的更好些,尽管我任然不能确信是否比之前好了很多。但是我将
UFO 绘图调用的次数减少到了仅仅 10 次左右。

图片 8

是否所有的绘图调用的减少都能让我们的游戏运行的跟快呢?不一定。
如果你能减少绘图调用,这很棒!但是对于那些更灵活,微妙的部分,如果你愿意牺牲一些灵活而去减少绘图调用,也不是不行。

目前我已经把游戏运行期间的平均 150-200 次的绘图调用减少到了仅仅 75-90
次。这已经减少了很多次了!

最后一部分,UFO
射出的激光,我在深入的研究后也解决了绘图调用的问题。所有的激光也都有素材引用,这些UFO的射击间隔很短“哒-哒”。在全力射击下每个激光会有30-40次的绘图调用。还好,这比创建初始方块要容易多了,使用相同的无光源顶点颜色着色器,再分配一些顶点颜色到网格就好了。现在所有的激光只需要一次绘图调用,就算是UFO“哒-哒-哒”不停地射击也没问题。
图片 9

现在我已经将整个游戏的绘图调用降低到 30-45 次了,怎么样?还行吧!

其他的绘图调用是 UI 引起的,我本来打算减少 UI
对象的数量来提高速度,但是我现在的效果我觉得挺好的了。游戏运行的比以前更流畅了。

图片 10 UFO
也使用了很少的绘图调用。但是想想大约 10 次和 30
次比较,还是有很大的区别的。

减少绘图调用的最重要的规则是使用尽可能少的素材。如果可以的话尽量使用共享素材而不是引用的素材,这些一定会帮助你减少你的绘图调用。

1. Watch for OpenGL ES Errors in Xcode and Instruments – 在Xcode和Instruments中观察OpenGL ES错误

当您的应用程序使用OpenGL ES API错误(例如,通过请求操作底层硬件无法执行)时,会出现OpenGL
ES错误。 即使您的内容正确呈现,这些错误也可能表明性能问题。 检查OpenGL
ES错误的传统方法是调用glGetError函数;
但是,重复调用此功能可能会显着降低性能。 应该使用上述工具来测试错误:

  • 在Instruments中分析您的应用程序时,请参阅OpenGL ES
    Analyzer工具的详细信息窗格,查看录制时报告的任何OpenGL ES错误。
  • Xcode中调试应用程序时,捕获一个帧以检查用于生成它的绘图命令,以及执行这些命令时遇到的任何错误。

当遇到OpenGL ES错误时,还可以配置Xcode以停止程序执行。
(请参阅Adding an OpenGL ES Error Breakpoint。)

小行星带

想象这样一个场景,在宇宙中有一个大的行星,它位于小行星带的中央。这样的小行星带可能包含成千上万的岩块,在很不错的显卡上也很难完成这样的渲染。实例化渲染正是适用于这样的场景,因为所有的小行星都可以使用一个模型来表示。每个小行星可以再使用不同的变换矩阵来进行少许的变化。

为了展示实例化渲染的作用,我们首先会不使用实例化渲染,来渲染小行星绕着行星飞行的场景。这个场景将会包含一个大的行星模型,它可以在这里下载,以及很多环绕着行星的小行星。小行星的岩石模型可以在这里下载。

在代码例子中,我们将使用在模型加载小节中定义的模型加载器来加载模型。

为了得到想要的效果,我们将会为每个小行星生成一个变换矩阵,用作它们的模型矩阵。变换矩阵首先将小行星位移到小行星带中的某处,我们还会加一个小的随机偏移值到这个偏移量上,让这个圆环看起来更自然一点。接下来,我们应用一个随机的缩放,并且以一个旋转向量为轴进行一个随机的旋转。最终的变换矩阵不仅能将小行星变换到行星的周围,而且会让它看起来更自然,与其它小行星不同。最终的结果是一个布满小行星的圆环,其中每一个小行星都与众不同。

unsigned int amount = 1000;
glm::mat4 *modelMatrices;
modelMatrices = new glm::mat4[amount];
srand(glfwGetTime()); // 初始化随机种子    
float radius = 50.0;
float offset = 2.5f;
for(unsigned int i = 0; i < amount; i++)
{
    glm::mat4 model;
    // 1. 位移:分布在半径为 'radius' 的圆形上,偏移的范围是 [-offset, offset]
    float angle = (float)i / (float)amount * 360.0f;
    float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float x = sin(angle) * radius + displacement;
    displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float y = displacement * 0.4f; // 让行星带的高度比x和z的宽度要小
    displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
    float z = cos(angle) * radius + displacement;
    model = glm::translate(model, glm::vec3(x, y, z));

    // 2. 缩放:在 0.05 和 0.25f 之间缩放
    float scale = (rand() % 20) / 100.0f + 0.05;
    model = glm::scale(model, glm::vec3(scale));

    // 3. 旋转:绕着一个(半)随机选择的旋转轴向量进行随机的旋转
    float rotAngle = (rand() % 360);
    model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

    // 4. 添加到矩阵的数组中
    modelMatrices[i] = model;
}  

这段代码看起来可能有点吓人,但我们只是将小行星的x和z位置变换到了一个半径为radius的圆形上,并且在半径的基础上偏移了-offset到offset。我们让y偏移的影响更小一点,让小行星带更扁平一点。接下来,我们应用了缩放和旋转变换,并将最终的变换矩阵储存在modelMatrices中,这个数组的大小是amount。这里,我们一共生成1000个模型矩阵,每个小行星一个。

在加载完行星和岩石模型,并编译完着色器之后,渲染的代码看起来是这样的:

// 绘制行星
shader.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
shader.setMat4("model", model);
planet.Draw(shader);

// 绘制小行星
for(unsigned int i = 0; i < amount; i++)
{
    shader.setMat4("model", modelMatrices[i]);
    rock.Draw(shader);
}  

我们首先绘制了行星的模型,并对它进行位移和缩放,以适应场景,接下来,我们绘制amount数量的岩石模型。在绘制每个岩石之前,我们首先需要在着色器内设置对应的模型变换矩阵。

最终的结果是一个看起来像是太空的场景,环绕着行星的是看起来很自然的小行星带:

图片 11

这个场景每帧包含1001次渲染调用,其中1000个是岩石模型。你可以在这里找到源代码。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <[learnopengl/shader.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader.h)>  
#include <[learnopengl/camera.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)> 
#include <[learnopengl/model.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/model.h)>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 55.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // -------------------------
    Shader shader("10.2.instancing.vs", "10.2.instancing.fs");
    // load models
    // -----------
    Model rock(FileSystem::getPath("resources/objects/rock/rock.obj"));
    Model planet(FileSystem::getPath("resources/objects/planet/planet.obj"));

    // generate a large list of semi-random model transformation matrices
    // ------------------------------------------------------------------
    unsigned int amount = 1000;
    glm::mat4* modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(glfwGetTime()); // initialize random seed 
    float radius = 50.0;
    float offset = 2.5f;
    for (unsigned int i = 0; i < amount; i++)
    {
        glm::mat4 model;
        // 1. translation: displace along circle with 'radius' in range [-offset, offset]
        float angle = (float)i / (float)amount * 360.0f;
        float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float x = sin(angle) * radius + displacement;
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float y = displacement * 0.4f; // keep height of asteroid field smaller compared to width of x and z
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float z = cos(angle) * radius + displacement;
        model = glm::translate(model, glm::vec3(x, y, z));

        // 2. scale: Scale between 0.05 and 0.25f
        float scale = (rand() % 20) / 100.0f + 0.05;
        model = glm::scale(model, glm::vec3(scale));

        // 3. rotation: add random rotation around a (semi)randomly picked rotation axis vector
        float rotAngle = (rand() % 360);
        model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

        // 4. now add to list of matrices
        modelMatrices[i] = model;
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // configure transformation matrices
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
        glm::mat4 view = camera.GetViewMatrix();;
        shader.use();
        shader.setMat4("projection", projection);
        shader.setMat4("view", view);

        // draw planet
        glm::mat4 model;
        model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
        model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
        shader.setMat4("model", model);
        planet.Draw(shader);

        // draw meteorites
        for (unsigned int i = 0; i < amount; i++)
        {
            shader.setMat4("model", modelMatrices[i]);
            rock.Draw(shader);
        }     

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

当我们开始增加这个数字的时候,你很快就会发现场景不再能够流畅运行了,帧数也下降很厉害。当我们将amount设置为2000的时候,场景就已经慢到移动都很困难的程度了。

现在,我们来尝试使用实例化渲染来渲染相同的场景。我们首先对顶点着色器进行一点修改:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 instanceMatrix;

out vec2 TexCoords;

uniform mat4 projection;
uniform mat4 view;

void main()
{
    gl_Position = projection * view * instanceMatrix * vec4(aPos, 1.0); 
    TexCoords = aTexCoords;
}

我们不再使用模型uniform变量,改为一个mat4的顶点属性,让我们能够存储一个实例化数组的变换矩阵。然而,当我们顶点属性的类型大于vec4时,就要多进行一步处理了。顶点属性最大允许的数据大小等于一个vec4。因为一个mat4本质上是4个vec4,我们需要为这个矩阵预留4个顶点属性。因为我们将它的位置值设置为3,矩阵每一列的顶点属性位置值就是3、4、5和6。

接下来,我们需要为这4个顶点属性设置属性指针,并将它们设置为实例化数组:

// 顶点缓冲对象
unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    unsigned int VAO = rock.meshes[i].VAO;
    glBindVertexArray(VAO);
    // 顶点属性
    GLsizei vec4Size = sizeof(glm::vec4);
    glEnableVertexAttribArray(3); 
    glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)0);
    glEnableVertexAttribArray(4); 
    glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(vec4Size));
    glEnableVertexAttribArray(5); 
    glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(2 * vec4Size));
    glEnableVertexAttribArray(6); 
    glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, 4 * vec4Size, (void*)(3 * vec4Size));

    glVertexAttribDivisor(3, 1);
    glVertexAttribDivisor(4, 1);
    glVertexAttribDivisor(5, 1);
    glVertexAttribDivisor(6, 1);

    glBindVertexArray(0);
}  

注意这里我们将MeshVAO从私有变量改为了公有变量,让我们能够访问它的顶点数组对象。这并不是最好的解决方案,只是为了配合本小节的一个简单的改动。除此之外代码就应该很清楚了。我们告诉了OpenGL应该如何解释每个缓冲顶点属性的缓冲,并且告诉它这些顶点属性是实例化数组。

接下来,我们再次使用网格的VAO,这一次使用glDrawElementsInstanced进行绘制:

// 绘制小行星
instanceShader.use();
for(unsigned int i = 0; i < rock.meshes.size(); i++)
{
    glBindVertexArray(rock.meshes[i].VAO);
    glDrawElementsInstanced(
        GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount
    );
}

这里,我们绘制与之前相同数量amount的小行星,但是使用的是实例渲染。结果应该是非常相似的,但如果你开始增加amount变量,你就能看见实例化渲染的效果了。没有实例化渲染的时候,我们只能流畅渲染1000到1500个小行星。而使用了实例化渲染之后,我们可以将这个值设置为100000,每个岩石模型有576个顶点,每帧加起来大概要绘制5700万个顶点,但性能却没有受到任何影响!

图片 12

上面这幅图渲染了10万个小行星,半径为150.0f,偏移量等于25.0f。你可以在这里找到实例化渲染的代码。

#include <glad/glad.h>  
#include <GLFW/glfw3.h>  
#include <stb_image.h>  
#include <glm/glm.hpp>  
#include <glm/gtc/matrix_transform.hpp>  
#include <glm/gtc/type_ptr.hpp>  
#include <[learnopengl/shader.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/shader.h)>  
#include <[learnopengl/camera.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/camera.h)>  
#include <[learnopengl/model.h](https://learnopengl.com/code_viewer_gh.php?code=includes/learnopengl/model.h)>  
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

// settings
const unsigned int SCR_WIDTH = 1280;
const unsigned int SCR_HEIGHT = 720;

// camera
Camera camera(glm::vec3(0.0f, 0.0f, 155.0f));
float lastX = (float)SCR_WIDTH / 2.0;
float lastY = (float)SCR_HEIGHT / 2.0;
bool firstMouse = true;

// timing
float deltaTime = 0.0f;
float lastFrame = 0.0f;

int main()
{
    // glfw: initialize and configure
    // ------------------------------
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif

    // glfw window creation
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    glfwSetCursorPosCallback(window, mouse_callback);
    glfwSetScrollCallback(window, scroll_callback);

    // tell GLFW to capture our mouse
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

    // glad: load all OpenGL function pointers
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // configure global opengl state
    // -----------------------------
    glEnable(GL_DEPTH_TEST);

    // build and compile shaders
    // -------------------------
    Shader asteroidShader("[10.3.asteroids.vs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.asteroids.vs)", "[10.3.asteroids.fs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.asteroids.fs)"); 
    Shader planetShader("[10.3.planet.vs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.planet.vs)", "[10.3.planet.fs](https://learnopengl.com/code_viewer_gh.php?code=src/4.advanced_opengl/10.3.asteroids_instanced/10.3.planet.fs)");
   // load models
    // -----------
    Model rock(FileSystem::getPath("resources/objects/rock/rock.obj"));
    Model planet(FileSystem::getPath("resources/objects/planet/planet.obj"));

    // generate a large list of semi-random model transformation matrices
    // ------------------------------------------------------------------
    unsigned int amount = 100000;
    glm::mat4* modelMatrices;
    modelMatrices = new glm::mat4[amount];
    srand(glfwGetTime()); // initialize random seed 
    float radius = 150.0;
    float offset = 25.0f;
    for (unsigned int i = 0; i < amount; i++)
    {
        glm::mat4 model;
        // 1. translation: displace along circle with 'radius' in range [-offset, offset]
        float angle = (float)i / (float)amount * 360.0f;
        float displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float x = sin(angle) * radius + displacement;
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float y = displacement * 0.4f; // keep height of asteroid field smaller compared to width of x and z
        displacement = (rand() % (int)(2 * offset * 100)) / 100.0f - offset;
        float z = cos(angle) * radius + displacement;
        model = glm::translate(model, glm::vec3(x, y, z));

        // 2. scale: Scale between 0.05 and 0.25f
        float scale = (rand() % 20) / 100.0f + 0.05;
        model = glm::scale(model, glm::vec3(scale));

        // 3. rotation: add random rotation around a (semi)randomly picked rotation axis vector
        float rotAngle = (rand() % 360);
        model = glm::rotate(model, rotAngle, glm::vec3(0.4f, 0.6f, 0.8f));

        // 4. now add to list of matrices
        modelMatrices[i] = model;
    }

    // configure instanced array
    // -------------------------
    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, amount * sizeof(glm::mat4), &modelMatrices[0], GL_STATIC_DRAW);

    // set transformation matrices as an instance vertex attribute (with divisor 1)
    // note: we're cheating a little by taking the, now publicly declared, VAO of the model's mesh(es) and adding new vertexAttribPointers
    // normally you'd want to do this in a more organized fashion, but for learning purposes this will do.
    // -----------------------------------------------------------------------------------------------------------------------------------
    for (unsigned int i = 0; i < rock.meshes.size(); i++)
    {
        unsigned int VAO = rock.meshes[i].VAO;
        glBindVertexArray(VAO);
        // set attribute pointers for matrix (4 times vec4)
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)0);
        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(sizeof(glm::vec4)));
        glEnableVertexAttribArray(5);
        glVertexAttribPointer(5, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(2 * sizeof(glm::vec4)));
        glEnableVertexAttribArray(6);
        glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(glm::mat4), (void*)(3 * sizeof(glm::vec4)));

        glVertexAttribDivisor(3, 1);
        glVertexAttribDivisor(4, 1);
        glVertexAttribDivisor(5, 1);
        glVertexAttribDivisor(6, 1);

        glBindVertexArray(0);
    }

    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        // --------------------
        float currentFrame = glfwGetTime();
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // configure transformation matrices
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 1000.0f);
        glm::mat4 view = camera.GetViewMatrix();
        asteroidShader.use();
        asteroidShader.setMat4("projection", projection);
        asteroidShader.setMat4("view", view);
        planetShader.use();
        planetShader.setMat4("projection", projection);
        planetShader.setMat4("view", view);

        // draw planet
        glm::mat4 model;
        model = glm::translate(model, glm::vec3(0.0f, -3.0f, 0.0f));
        model = glm::scale(model, glm::vec3(4.0f, 4.0f, 4.0f));
        planetShader.setMat4("model", model);
        planet.Draw(planetShader);

        // draw meteorites
        asteroidShader.use();
        asteroidShader.setInt("texture_diffuse1", 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, rock.textures_loaded[0].id); // note: we also made the textures_loaded vector public (instead of private) from the model class.
        for (unsigned int i = 0; i < rock.meshes.size(); i++)
        {
            glBindVertexArray(rock.meshes[i].VAO);
            glDrawElementsInstanced(GL_TRIANGLES, rock.meshes[i].indices.size(), GL_UNSIGNED_INT, 0, amount);
            glBindVertexArray(0);
        }

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);

    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
        camera.ProcessKeyboard(FORWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
        camera.ProcessKeyboard(BACKWARD, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
        camera.ProcessKeyboard(LEFT, deltaTime);
    if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
        camera.ProcessKeyboard(RIGHT, deltaTime);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and 
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

// glfw: whenever the mouse moves, this callback is called
// -------------------------------------------------------
void mouse_callback(GLFWwindow* window, double xpos, double ypos)
{
    if (firstMouse)
    {
        lastX = xpos;
        lastY = ypos;
        firstMouse = false;
    }

    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos; // reversed since y-coordinates go from bottom to top

    lastX = xpos;
    lastY = ypos;

    camera.ProcessMouseMovement(xoffset, yoffset);
}

// glfw: whenever the mouse scroll wheel scrolls, this callback is called
// ----------------------------------------------------------------------
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset)
{
    camera.ProcessMouseScroll(yoffset);
}

在某些机器上,10万个小行星可能会太多了,所以尝试修改这个值,直到达到一个你能接受的帧率。

可以看到,在合适的环境下,实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因,实例化渲染通常会用于渲染草、植被、粒子,以及上面这样的场景,基本上只要场景中有很多重复的形状,都能够使用实例化渲染来提高性能。

确保你只加载了一次资源

在我的代码中我使用了一下资源加载,但是 Unity
没有缓存加载结果,因此导致了多次的加载了相同的资源。这个功能消耗了大量的性能。
我以前也是多次的加载了相同的素材到我的激光武器上,像往常一样游戏在电脑上运行的十分好,但是在
Android 上,就不行了。

最终我避免了重复加载资源,并且删掉了项目中资源加载的地方。但是在这之前,我创建了一个静态的字典,字符串作为
Key(资源的名称),资源作为
Value,然后,我使用的时候都会检查字典里是否已经存在
Key,如果没有就加载资源,否则从字典缓存里获取资源。

我建议你可以试着用这个方法加载舞台。

2. Annotate Your OpenGL ES Code for Informative Debugging and Profiling – 注释您的OpenGL ES代码进行信息调试和分析

您可以通过将OpenGL ES命令组织到逻辑组中并为OpenGL
ES对象添加有意义的标签来使调试和分析更加高效。
这些组和标签出现在Xcode中的OpenGL ES Frame Debugger中,如下图所示,在仪器中的OpenGL ES Analyzer中。
要添加组和标签,请使用EXT_debug_markerEXT_debug_label扩展。

图片 13

Xcode Frame Debugger before and after adding debug marker groups

当您有一系列绘图命令代表一个有意义的操作 –
例如绘制游戏角色时,您可以使用标记将其分组进行调试。
下面代码显示了如何对纹理,程序,顶点数组和场景的单个元素绘制调用。
首先,它调用glPushGroupMarkerEXT函数来提供有意义的名称,然后发出一组OpenGL
ES命令。 最后,它关闭组,并调用glPopGroupMarkerEXT函数。

// Using a debug marker to annotate drawing commands

glPushGroupMarkerEXT(0, "Draw Spaceship");
glBindTexture(GL_TEXTURE_2D, _spaceshipTexture);
glUseProgram(_diffuseShading);
glBindVertexArrayOES(_spaceshipMesh);
glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT, 0);
glPopGroupMarkerEXT();

您可以使用多个嵌套标记来在复杂场景中创建有意义的组的层次结构。
当您使用GLKView类绘制OpenGL
ES内容时,它会自动创建一个“渲染”组,其中包含绘图方法中的所有命令。
您创建的任何标记都嵌套在此组内。

标签为OpenGL
ES对象提供有意义的名称,例如纹理,着色器程序和顶点数组对象。
调用glLabelObjectEXT函数为对象提供调试和分析时要显示的名称。
下面代码说明了使用这个函数来标记一个顶点数组对象。
如果您使用GLKTextureLoader类加载纹理数据,它会自动标记其使用其文件名创建的OpenGL
ES纹理对象。

// Using a debug label to annotate an OpenGL ES object

glGenVertexArraysOES(1, &_spaceshipMesh);

glBindVertexArrayOES(_spaceshipMesh);

glLabelObjectEXT(GL_VERTEX_ARRAY_OBJECT_EXT, _spaceshipMesh, 0, "Spaceship");

后记

本篇结束,下一篇与抗锯齿有关。

图片 14

尽可能避免实例化

我从来没有考虑过对象的实例化会消耗多少性能,我几乎在所有地方都实例化了。我只是觉得它跟创建一个新的类的引用消耗的性能类似。但是我错了,事实上,程序花费了一些时间在
CPU 上实例化一个对象,又花费相同时间去销毁这个对象。问题在于我发现每次
UFO
攻击的时候,他妈的都会让我创建大量的激光。每个激光的实例化和销毁间隔很短很短的时间。我算了一下,大约
20-40 对象的实例化和销毁耗时 1.5
秒。减少了绘图调用是很好,但是我从未意识到 UFO
出现后实际上是实例化消耗了大量的性能。

能解决这个问题的唯一的办法是创建有序的对象池。我在场景里面创建了一个新的空的对象并调用ProjectilePool。在代码里创建了一些新的
Projectiles ,我废弃了以前在 Projectiles  list
里去查找Projectile,而是在 ProjectilePool 里线查找有没有可用的
Projectile,如果有,就取得这个 Projectile
并且从新设置它的位置和状态。这样就能从新使用这个就旧的 Projectile 了。

如果我没有在 List 中找到 Projectile,我就像以前一样创建一个。但是这时
Projectile 通常会被销毁掉,而我把 Projectile 添加到
ProjectilePool 并且使它不活动。因此我现在可以将 UFO 攻击期间的CPU
的使用率降低到几乎 25%-30%。现在我的游戏运行的超级好。

General Performance Recommendations – 一般性能推荐

使用常识来指导您的性能调整工作。
例如,如果您的应用程序每帧仅绘制几十个三角形,更改提交顶点数据的方式将不太可能提高其性能。
寻找为您的努力提供最佳性能的优化。

总结

绘图调用等于怪兽。如果你想尽可能的减少。最好的方式是面对他们,减少你的对象使用的素材的数量。使用较少不同的纹理,试着并且调整尽量多的纹理到地图集中。如果你正在实现
2D
并且不要使用太多的光源或者像我一样,只使用只有一种颜色的纹理。然后使用使用无光源顶点颜色,没有任何参数可以被用到所有的类。这很可能减少很多次绘图调用。如果你愿意牺牲一漂亮的视觉效果,你也可以合并网格或许这还是有用的。

实例化很慢,非常慢。试着尽可能的避免实例化。试着在初始化的时候加载尽肯能多的对象,然后当你想使用的时候在引用它们。另一个很好的方式用一个对象池循环的使用旧的对象来减少实例化的数量。

当然绘图调用和实例化不仅仅是唯一的恶棍。你得记着绘图调用使用了 CPU 和
GPU,而实例化使用了 CPU。

如果在你的游戏中你有大的复杂的模块或者太多的处理要运行。仅仅减少绘图调用是不能帮助你提供速度,当然这也会使你的游戏运行的快些但不总是这样。CPU
有时是你最大的敌人。先看看你的代码,然后试着找出运行的糟糕的地方然后让它运行的更好些。

在我的游戏中,实例化,销毁和 Web 请求时最大问题。

1. Redraw Scenes Only When the Scene Data Changes – 仅当场景数据更改时才重画场景

您的应用程序应该等待,直到场景中的某些内容发生变化才能渲染新的帧
核心动画缓存呈现给用户的最后一张图像,并继续显示,直到出现新的帧。

即使您的数据发生变化,也不需要以硬件处理命令的速度渲染帧。
对于用户来说,速度较慢但固定的帧速率通常比快速但可变的帧速率更平滑。
每秒30帧的固定帧速率对于大多数动画是足够的,并且有助于降低功耗。

这篇文章的结束只是下一篇开始

Sky Blocks 在 Google Play 上的下载地址

我使用的无光源顶点着色器

减少绘图调用到底有多重要?

       
“虽然绘图调用可以成为一个瓶颈,但是记住帧频才是王道。如果你的帧频是够好,那就没必要担心绘图调用。绘图调用被请求的数量是否严重的影响了性能,很大程度上取决于硬件的状况和每一帧所做的所有的事情”
— Daniel Brauer, Unity Technologies

实例化素材 VS 共享素材

实例化素材的主要的特点是一个可以让任何属性改变的素材。一个实例化素材仅仅为了一个特殊的类被实例化一次。每次的实例化都有可能会触发一次绘图调用。但是实例化之后改变他的属性是不会创建新的实例的,而仅仅是修改了当前的实例。然而共享的素材是使用了相同的着色器和其他相同的属性的素材。Unity
是可以对素材分组并且批处理所有对象来使用这个素材。自从我用了无光源着色器就没有在代码中修改过任何属性,素材也从来没有被实例化过而所有的素材都是一起被批处理的。

注意了,我将要发布一篇较详细信息的文章来介绍对于不同的对象,我是如何提高性能的,包括更多的代码实例。

但是现在,祝大家永远开心,快乐。

2. Disable Unused OpenGL ES Features – 禁用未使用的OpenGL ES功能

最好的计算是您的应用程序从未执行的计算。
例如,如果结果可以预先计算并存储在模型数据中,则可以避免在运行时执行该计算。

如果您的应用程序是针对OpenGL ES
2.0或更高版本编写的,请勿创建一个具有大量开关和条件的单一着色器,以执行应用程序渲染场景所需的每个任务。
相反,编译多个着色器程序,每个着色器程序执行一个特定的,重点任务。

如果您的应用程序使用OpenGL ES
1.1,请禁用任何不需要渲染场景的固定功能操作。
例如,如果您的应用程序不需要照明或混合,请禁用这些功能。
同样,如果您的应用程序仅绘制2D模型,则应禁用雾度和深度测试。

3. Simplify Your Lighting Models – 简化您的照明模型

这些准则既适用于OpenGL ES 1.1中的固定功能照明,又适用于您在OpenGL ES
2.0或更高版本中的自定义着色器中使用的基于着色器的照明计算。

为您的应用程序使用最少的灯和最简单的照明类型。
考虑使用定向灯而不是点光源,点光源这需要更多的计算。
着色器应在模型空间中执行照明计算;
在更复杂的照明算法中,考虑在着色器中使用更简单的照明方程。

预先计算您的照明,并将颜色值存储在可通过片段处理进行采样的纹理中。


Use Tile-Based Deferred Rendering Efficiently – 有效地使用基于平铺的延迟渲染

iOS设备中使用的所有GPU都使用基于瓦片的延迟渲染(TBDR)。 当您调用OpenGL
ES函数向硬件提交呈现命令时,这些命令将被缓冲,直到累积了大量命令。
在呈现renderbuffer或刷新命令缓冲区之前,硬件不会开始处理顶点和阴影像素。
然后,它们将这些命令作为单个操作,通过将帧缓冲区划分为图块,然后为每个图块绘制一次命令,每个图块只渲染其中可见的图元。
(GLKView类在绘图方法返回后呈现renderbuffer,如果使用CAEAGLLayer类创建自己的renderbuffer来显示,则使用OpenGL
ES上下文的presentRenderbuffer:方法来呈现它,glFlushglFinish函数刷新命令缓冲区。

由于瓦片内存是GPU硬件的一部分,渲染过程的一部分(如深度测试和混合)在时间和能量使用方面比传统的基于流的GPU架构更为高效。
因为这个架构一次处理整个场景的所有顶点,GPU可以在片段被处理之前执行隐藏的表面去除。
不可见的像素在没有采样纹理或执行片段处理的情况下被丢弃,大大减少了GPU必须执行的渲染图块的计算。

在传统的基于流的渲染器上有用的渲染策略在iOS图形硬件上具有高性能成本。
遵循以下准则可以帮助您的应用在TBDR硬件上表现良好。

1. Avoid Logical Buffer Loads and Stores – 避免逻辑缓冲区负载和存储

当TBDR图形处理器开始渲染图块时,必须首先将帧缓冲区的该部分的内容从共享内存传输到GPU上的瓦片内存。
这种内存传输,称为逻辑缓冲区负载,需要时间和精力。
大多数情况下,帧缓冲区的以前内容对于绘制下一帧是不必要的。
当您开始渲染新的帧时,通过调用glClear避免加载先前缓冲区内容带来的性能成本。

类似地,当GPU完成绘制瓦片时,它必须将瓦片的像素数据写回共享存储器。
这种称为逻辑缓冲存储器的传输也具有性能成本。
对于每个绘制的画面,至少需要进行一次这样的转移,屏幕上显示的彩色渲染缓冲区必须被传送到共享存储器,以便Core
Animation可以呈现。
在渲染算法中使用的其他帧缓冲附件(例如,深度,模板和多采样缓冲区)不需要保留,因为它们的内容将在下一帧绘制后重新创建。
OpenGL ES会自动将这些缓冲区存储到共享内存中,从而导致性能成本 –
除非您明确使其无效。 要使缓冲区无效,请使用OpenGL ES
3.0中的glInvalidateFramebuffer命令或OpenGL ES
1.1或2.0中的glDiscardFramebufferEXT命令。
(有关详细信息,请参阅Discard Unneeded
Renderbuffers),当您使用GLKView类提供的基本绘图循环时,它会自动使任何可绘制的深度,模板或多重采样缓冲区无效。

如果切换渲染目的地,也会发生逻辑缓冲区存储和加载操作。
如果渲染到纹理,然后渲染到视图的帧缓冲区,然后再次渲染到相同的纹理,则纹理的内容必须在共享内存和GPU之间重复传输。
批量绘制操作,以便将所有绘制到渲染目的地一起完成。
当切换帧缓冲区(使用glBindFramebufferglFramebufferTexture2D函数或bindDrawable方法)时,会使不需要的帧缓冲附件无效,以避免导致逻辑缓冲区存储。

2. Use Hidden Surface Removal Effectively – 有效地使用隐藏的表面去除

TBDR图形处理器自动使用深度缓冲区为整个场景执行隐藏的表面删除,确保每个像素只运行一个片段着色器。
用于减少片段处理的传统技术不是必需的。
例如,通过深度从前到后排序对象或原语有效地复制了GPU完成的工作,浪费了CPU时间。

当混合或Alpha测试启用时,或者片段着色器使用丢弃指令或写入gl_FragDepth输出变量时,GPU无法执行隐藏的表面删除。
在这些情况下,GPU无法使用深度缓冲区来确定片段的可见性,因此必须对覆盖每个像素的所有图元运行片段着色器,从而大大增加渲染帧所需的时间和精力。
为了避免这种性能成本,最小化混合使用,丢弃指令和深度写入。

如果您无法避免混合,Alpha测试或丢弃说明,请考虑以下策略来降低其性能影响:

  • 按不透明度排序对象。 先绘制不透明物体。
    接下来,使用丢弃操作(或OpenGL ES
    1.1中的Alpha测试)绘制需要着色器的对象。 最后,绘制alpha混合对象。
  • 修剪需要混合或丢弃指令的对象以减少处理的碎片数量。
    例如,如下图所示,不是绘制一个正方形来渲染包含大部分空白空间的2D精灵纹理,而是绘制一个更贴近图像形状的多边形。
    附加顶点处理的性能成本远低于运行片段着色器,运行片段着色器的结果将不被使用。

图片 15

Trim transparent objects to reduce fragment processing

  • 在片段着色器中尽早使用丢弃指令,以避免执行结果未使用的计算。
  • 不使用alpha测试或丢弃指令来杀死像素,而是将alpha混合与Alpha设置为零。
    彩色帧缓冲区未被修改,但图形硬件仍然可以使用它执行的任何Z缓冲区优化。
    这样做会改变存储在深度缓冲区中的值,因此可能需要对透明基元进行前后排序。
  • 如果您的表现受到不可避免的丢弃操作限制,请考虑“Z-Prepass”渲染策略。
    使用简单的片段着色器渲染场景,只包含丢弃逻辑(避免昂贵的照明计算)来填充深度缓冲区。
    然后,使用GL_EQUAL深度测试功能和您的照明着色器再次渲染您的场景。
    虽然多次渲染通常会导致性能损失,但是这种方法可以产生比涉及大量丢弃操作的单遍渲染更好的性能。

3. Group OpenGL ES Commands for Efficient Resource Management – OpenGL ES命令用于高效的资源管理

上述内存带宽和计算节省在处理大型场景时表现最佳。
但是当硬件接收到需要渲染较小场景的OpenGL
ES命令时,渲染器的效率就大大降低。
例如,如果您的应用程序使用纹理渲染批次的三角形,然后修改纹理,则OpenGL
ES实现必须立即刷新这些命令,或者重复纹理,这两个选项会有效地使用硬件。
类似地,如果它们会改变该帧缓冲区,从帧缓冲区读取像素数据的任何尝试都要求处理前面的命令。

为了避免这些性能损失,请组织您的OpenGL
ES调用序列,以便一起执行每个渲染目标的所有绘图命令。


Minimize the Number of Drawing Commands – 最小化绘图命令的数量

每当您的应用程序提交要由OpenGL ES处理的图元时,CPU将准备图形硬件的命令。
如果您的应用程序使用许多glDrawArraysglDrawElements调用来渲染场景,则其性能可能受到CPU资源的限制,而不会完全利用GPU。

为了减少这种开销,寻找将渲染整合到较少绘图调用中的方法。
有用的策略包括:

  • 将多个基元合并成单个三角形条,如Use Triangle Strips to Batch Vertex
    Data中所述。
    为获得最佳效果,请合并在紧密的空间附近绘制的图元。
    大量,蔓延的模型更难以有效地剔除当他们在帧中不可见时。
  • 创建纹理地图集以使用相同纹理图像的不同部分绘制多个图元,如Combine
    Textures into Texture
    Atlases中所述。
  • 使用实例绘制来渲染许多类似的对象,如下所述。

1. Use Instanced Drawing to Minimize Draw Calls – 使用实例化绘图来最小化绘制调用

实例绘制命令允许您使用单个绘图调用多次绘制相同的顶点数据。
代替使用CPU时间来设置网格的不同实例(如位置偏移,变换矩阵,颜色或纹理坐标)之间的变化,并为每个实例绘制绘制命令,将实例变体的处理移动到着色器代码中
在GPU上运行

重复使用的顶点数据是实例绘制的主要候选者。
例如,下面代码中的代码在场景中的多个位置绘制一个对象。
然而,许多glUniformglDrawArrays调用增加了CPU开销,从而降低了性能。

// Drawing many similar objects without instancing

for (x = 0; x < 10; x++) {
    for (y = 0; y < 10; y++) {
        glUniform4fv(uniformPositionOffset, 1, positionOffsets[x][y]);
        glDrawArrays(GL_TRIANGLES, 0, numVertices);
    }
}

采用实例化绘图需要两个步骤:首先,如上所述替换循环,单次调用glDrawArraysInstancedglDrawElementsInstanced
这些调用与glDrawArraysglDrawElements相同,但附加参数指示要绘制的实例数(上面代码中的示例为100)。
其次,选择和实现OpenGL
ES为您的顶点着色器使用每个实例信息提供的两个策略之一。

使用着色器实例ID策略,您的顶点着色器会导出或查找每个实例信息。
每次顶点着色器运行时,其gl_InstanceID内置变量都包含一个标识当前正在绘制的实例的数字。
使用此数字计算着色器代码中的位置偏移,颜色或其他每个实例的变化,或者查找统一数组或其他大容量存储中的每个实例信息。
例如,下面代码使用此技术来绘制位于10 x 10网格中的100个网格实例。

// OpenGL ES 3.0 vertex shader using gl_InstanceID to compute per-instance information

#version 300 es

in vec4 position;

uniform mat4 modelViewProjectionMatrix;

void main()
{
    float xOffset = float(gl_InstanceID % 10) * 0.5 - 2.5;
    float yOffset = float(gl_InstanceID / 10) * 0.5 - 2.5;
    vec4 offset = vec4(xOffset, yOffset, 0, 0);

    gl_Position = modelViewProjectionMatrix * (position + offset);
}

通过实例化的数组策略,您可以将每个实例信息存储在顶点数组属性中。
您的顶点着色器可以访问该属性以使用每个实例信息。
调用glVertexAttribDivisor函数来指定OpenGL
ES绘制每个实例时该属性的进度。
下面两段代码的第一段演示了为实例绘制设置一个顶点数组,第二段显示了相应的着色器。

// Using a vertex attribute for per-instance information

#define kMyInstanceDataAttrib 5

glGenBuffers(1, &_instBuffer);

glBindBuffer(GL_ARRAY_BUFFER, _instBuffer);

glBufferData(GL_ARRAY_BUFFER, sizeof(instData), instData, GL_STATIC_DRAW);

glEnableVertexAttribArray(kMyInstanceDataAttrib);

glVertexAttribPointer(kMyInstanceDataAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);

glVertexAttribDivisor(kMyInstanceDataAttrib, 1);

// OpenGL ES 3.0 vertex shader using instanced arrays

#version 300 es

layout(location = 0) in vec4 position;
layout(location = 5) in vec2 inOffset;

uniform mat4 modelViewProjectionMatrix;

void main()
{
    vec4 offset = vec4(inOffset, 0.0, 0.0)
    gl_Position = modelViewProjectionMatrix * (position + offset);
}

实例绘图在核心OpenGL ES 3.0 APIOpenGL ES 2.0中通过EXT_draw_instancedEXT_instanced_arrays扩展提供。


Minimize OpenGL ES Memory Usage – 最小化OpenGL ES内存使用

您的iOS应用程序与系统和其他iOS应用程序共享主内存。 为OpenGL
ES分配的内存减少了您的应用程序中可用于其他用途的内存。
考虑到这一点,只需分配您需要的内存,并在应用程序不再需要它时立即释放它。
这里有几种方法可以节省内存:

  • 将图像加载到OpenGL ES纹理后,释放原始图像。
  • 只有在您的应用程序需要时才分配深度缓冲区。
  • 如果您的应用程序不需要一次所有资源,只需加载一部分项目。
    例如,一个游戏可能被分为几个级别;
    每个都加载适合更严格资源限制的总资源的子集。

iOS中的虚拟内存系统不使用交换文件。
当检测到低内存条件时,虚拟内存不会将易失性页面写入磁盘,而是释放非易失性内存,为运行中的应用程序提供所需的内存。
您的应用程序应尽可能少地使用内存,并准备处理对应用程序不是必需的对象。
针对低内存条件的响应在iOS的App Programming Guide for
iOS
中有详细介绍。


Be Aware of Core Animation Compositing Performance – 要注意Core Animation合成性能

Core Animation将renderbuffers的内容与视图层次结构中的任何其他图层相结合,无论这些图层是用OpenGL
ES,Quartz还是其他图形库绘制。 这很有帮助,因为这意味着OpenGL
ES是核心动画的first - class citizen。 然而,将OpenGL
ES内容与其他内容混合需要时间;
当使用不当时,您的应用程序可能执行得太慢,无法达到交互式帧速率。

为了获得最佳性能,您的应用程序应该仅依靠OpenGL ES来呈现您的内容。
将保存OpenGL
ES内容的视图设置为与屏幕匹配,确保其opaque属性设置为YES(GLKView对象的默认值),并且不显示其他视图或Core
Animation图层。

如果将其渲染为在其他图层之上合成的Core
Animation图层,则使CAEAGLLayer对象不透明减少但不会消除性能成本。
如果您的CAEAGLLayer对象在图层层次结构中的层之下混合在一起,则renderbuffer的颜色数据必须是由Core
Animation正确合成的预乘法alpha格式。 将OpenGL
ES内容混合在其他内容之上具有严重的性能损失。

后记

未完,待续~~~

图片 16

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图