C++ 游戏开发示例(三)

原文:zh.annas-archive.org/md5/262fd2303f01348f0fc754084f6f20af

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:基于 Game 对象构建

在最后一章,我们探讨了如何使用 OpenGL 绘制基本形状。现在我们已经掌握了基础知识,让我们通过给对象添加一些纹理来提高它们,这样对象就不会仅仅看起来像一个普通的立方体和球体。

我们可以像上次那样编写我们的物理代码,但是当处理 3D 对象时,编写自己的物理代码可能会变得困难且耗时。为了简化过程,我们将使用外部物理库来处理物理和碰撞检测。

我们将在本章中涵盖以下主题:

  • 创建 MeshRenderer

  • 创建 TextureLoader

  • 添加 Bullet 物理引擎

  • 添加刚体

创建 MeshRenderer

对于绘制常规游戏对象,我们将从 LightRenderer 类中创建一个单独的类,通过添加纹理,并且我们还将通过添加物理属性来给对象添加运动。我们将在本章的下一节中绘制一个纹理对象,并给这个对象添加物理。为此,我们将创建一个新的 .h.cpp 文件,名为 MeshRenderer

MeshRenderer.h 文件中,我们将执行以下操作:

  1. 首先,我们将添加以下包含:
#include <vector> 

#include "Camera.h" 
#include "LightRenderer.h" 

#include <GL/glew.h> 

#include "Dependencies/glm/glm/glm.hpp" 
#include "Dependencies/glm/glm/gtc/matrix_transform.hpp" 
#include "Dependencies/glm/glm/gtc/type_ptr.hpp" 
  1. 接下来,我们将创建类本身,如下所示:
Class MeshRenderer{  

}; 
  1. 我们首先创建 public 部分,如下所示:
   public: 
         MeshRenderer(MeshType modelType, Camera* _camera); 
         ~MeshRenderer(); 

          void draw(); 

         void setPosition(glm::vec3 _position); 
         void setScale(glm::vec3 _scale); 
         void setProgram(GLuint _program); 
         void setTexture(GLuint _textureID); 

在本节中,我们创建一个构造函数,它接受 ModelType_camera。之后,我们添加析构函数。我们有一个单独的函数用于绘制对象。

  1. 然后我们使用一些 setter 函数来设置位置、缩放、着色器程序以及 textureID 函数,我们将使用它来设置对象上的纹理。

  2. 接下来,我们将添加 private 部分,如下所示:

   private: 

         std::vector<Vertex>vertices; 
         std::vector<GLuint>indices; 
         glm::mat4 modelMatrix; 

         Camera* camera; 

         glm::vec3 position, scale; 

               GLuint vao, vbo, ebo, texture, program;  

private 部分,我们有向量来存储顶点和索引。然后,我们有一个名为 modelMatrixglm::mat4 变量,用于存储模型矩阵值。

  1. 我们为相机创建一个局部变量,并为存储位置和缩放值创建 vec3s

  2. 最后,我们有 Gluint 来存储 vaovboebotextureID 和着色器程序。

我们将接着通过以下步骤来设置 MeshRenderer.cpp 文件:

  1. 首先,我们将在 MeshRenderer.cpp 的顶部包含 MeshRenderer.h 文件。

  2. 接下来,我们将为 MeshRenderer 创建构造函数,如下所示:

MeshRenderer::MeshRenderer(MeshType modelType, Camera* _camera) { 

} 
  1. 为了这个,我们首先初始化 camerapositionscale 本地值,如下所示:
   camera = _camera; 

   scale = glm::vec3(1.0f, 1.0f, 1.0f); 
   position = glm::vec3(0.0, 0.0, 0.0);
  1. 然后我们创建一个 switch 语句,就像我们在 LightRenderer 中做的那样,以获取网格数据,如下所示:
   switch (modelType){ 

         case kTriangle: Mesh::setTriData(vertices, indices);  
               break; 
         case kQuad: Mesh::setQuadData(vertices, indices);  
               break; 
         case kCube: Mesh::setCubeData(vertices, indices); 
               break; 
         case kSphere: Mesh::setSphereData(vertices, indices);  
               break; 
   } 
  1. 然后,我们生成并绑定 vaovboebo。此外,我们按照以下方式设置 vboebo 的数据:
   glGenVertexArrays(1, &vao); 
   glBindVertexArray(vao); 

   glGenBuffers(1, &vbo); 
   glBindBuffer(GL_ARRAY_BUFFER, vbo); 
   glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex) * vertices.size(),
   &vertices[0], GL_STATIC_DRAW); 

   glGenBuffers(1, &ebo); 
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); 
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLuint) * 
      indices.size(), &indices[0], GL_STATIC_DRAW); 
  1. 下一步是设置属性。在这种情况下,我们将设置 position 属性,但不是颜色,我们将设置纹理坐标属性,因为它将用于在对象上设置纹理。

  2. 0 索引处的属性仍然是一个顶点位置,但这次第一个索引处的属性将是一个纹理坐标,如下面的代码所示:

glEnableVertexAttribArray(0);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 
   (GLvoid*)0);

glEnableVertexAttribArray(1);

glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),   
   (void*)(offsetof(Vertex, Vertex::texCoords)));

在这里,顶点位置的属性保持不变,但对于纹理坐标,第一个索引如之前一样被启用。变化发生在组件数量上。纹理坐标在x轴和y轴上定义,因为这是一个 2D 纹理,所以对于第二个参数,我们指定2而不是3。步长仍然保持不变,但偏移量改为texCoords

  1. 为了关闭构造函数,我们解绑缓冲区和vertexArray,如下所示:
glBindBuffer(GL_ARRAY_BUFFER, 0); 
glBindVertexArray(0); 
  1. 我们现在添加draw函数,如下所示:
void MeshRenderer::draw() { 

} 

  1. 在这个draw函数中,我们首先将模型矩阵设置为以下内容:

   glm::mat4 TranslationMatrix = glm::translate(glm::mat4(1.0f),  
      position); 

   glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale); 

   modelMatrix = glm::mat4(1.0f); 

   modelMatrix = TranslationMatrix *scaleMatrix; 
  1. 我们将创建两个矩阵来存储translationMatrixscaleMatrix,然后设置它们的值。

  2. 然后我们将初始化modelMatrix变量,将缩放和变换矩阵相乘,并将它们赋值给modelMatrix变量。

  3. 接下来,我们不再创建单独的视图和投影矩阵,而是可以创建一个名为vp的单个矩阵,并将乘积的视图和投影矩阵赋值给它,如下所示:

glm::mat4 vp = camera->getprojectionMatrix() * camera->
               getViewMatrix(); 

显然,视图和投影矩阵相乘的顺序很重要,不能颠倒。

  1. 我们现在可以将值发送到 GPU。

  2. 在我们将值发送到着色器之前,我们必须做的第一件事是调用glUseProgram并设置着色器程序,以便数据被发送到正确的程序。一旦完成,我们就可以设置vpmodelMatrix的值,如下所示:

glUseProgram(this->program); 

GLint vpLoc = glGetUniformLocation(program, "vp"); 
glUniformMatrix4fv(vpLoc, 1, GL_FALSE, glm::value_ptr(vp)); 

GLint modelLoc = glGetUniformLocation(program, "model"); 
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));  
  1. 接下来,我们将绑定texture对象。我们使用glBindTexture函数来绑定纹理。该函数接受两个参数,第一个是纹理目标。我们有一个 2D 纹理,因此我们将GL_TEXTURE_2D作为第一个参数传递,并将纹理 ID 作为第二个参数。为此,我们添加以下行来绑定纹理:
glBindTexture(GL_TEXTURE_2D, texture);  

你可能想知道为什么在设置纹理位置时我们没有使用glUniformMatrix4fv或类似函数,就像我们为矩阵所做的那样。嗯,因为我们只有一个纹理,程序默认将统一位置设置为 0 索引,所以我们不必担心这一点。这就是我们绑定纹理所需的所有内容。

  1. 接下来,我们可以绑定vao并绘制对象,如下所示:
glBindVertexArray(vao);           
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);   
  1. 最后,按照以下方式解绑VertexArray
glBindVertexArray(0); 

  1. 接下来,我们将添加析构函数和setters的定义,如下所示:
MeshRenderer::~MeshRenderer() { 

} 

// setters  

void MeshRenderer::setTexture(GLuint textureID) { 

   texture = textureID; 

} 

void MeshRenderer::setScale(glm::vec3 _scale) { 

   this->scale = _scale; 
} 

void MeshRenderer::setPosition(glm::vec3 _position) { 

   this->position = _position; 
} 

void MeshRenderer::setProgram(GLuint _program) { 

   this->program = _program; 
} 

创建 TextureLoader 类

我们创建了MeshRenderer类,但我们仍然需要加载纹理并设置纹理 ID,这可以传递给MeshRenderer对象。为此,我们将创建一个TextureLoader类,该类将负责加载纹理。让我们看看如何做到这一点。

我们首先需要创建一个新的.h.cpp文件,名为TextureLoader

要加载 JPEG 或 PNG 图像,我们将使用一个仅包含头文件的库,称为 STB。可以从github.com/nothings/stb下载。从链接克隆或下载源代码,并将stb-master文件夹放置在Dependencies文件夹中。

TextureLoader类中,添加以下内容:

#include <string> 
#include <GL/glew.h> 

class TextureLoader 
{ 
public: 
   TextureLoader(); 

   GLuint getTextureID(std::string  texFileName); 
   ~TextureLoader(); 
}; 

然后,我们将使用stringglew.h库,因为我们将会传递 JPEG 所在文件的路径,STB将从那里加载文件。我们将添加构造函数和析构函数,因为它们是必需的;否则,编译器会给出错误。然后,我们将创建一个名为getTextureID的函数,它接受一个字符串作为输入并返回GLuint,这将作为纹理 ID。

TextureLoader.cpp文件中,我们包含了TextureLoader.h。然后添加以下代码以包含STB

#define STB_IMAGE_IMPLEMENTATION 
#include "Dependencies/stb-master/stb_image.h" 

我们添加#define,因为它在TextureLoader.cpp文件中是必需的,导航到stb_image.h,并将其包含到项目中。然后添加构造函数和析构函数,如下所示:


TextureLoader::TextureLoader(){ 

} 

TextureLoader::~TextureLoader(){ 

} 

接下来,我们创建getTextureID函数,如下所示:

GLuint TextureLoader::getTextureID(std::string texFileName){ 

}  

getTextureID函数中,我们首先创建三个int变量来存储宽度、高度和通道数。图像通常只有三个通道:红色、绿色和蓝色。然而,它可能有一个第四个通道,即 alpha 通道,用于透明度。JPEG 图片只有三个通道,但 PNG 文件可能有三个或四个通道。

在我们的游戏中,我们只会使用 JPEG 文件,因此channels参数始终为三个,如下代码所示:

   int width, height, channels;  

我们将使用stbi_load函数将图像数据加载到无符号字符指针中,如下所示:

stbi_uc* image = stbi_load(texFileName.c_str(), &width, &height,   
                 &channels, STBI_rgb); 

函数接受五个参数。第一个是文件/文件名的字符串。然后,它作为第二、第三和第四个参数返回宽度、高度和通道数,并在第五个参数中设置所需的组件。在这种情况下,我们只想有rgb通道,所以我们指定STBI_rgb

然后,我们必须按照以下方式生成和绑定纹理:

GLuint mtexture; 
glGenTextures(1, &mtexture); 
glBindTexture(GL_TEXTURE_2D, mtexture);    

首先,创建一个名为mtextureGLuint类型的纹理 ID。然后,我们调用glGenTextures函数,传入我们想要创建的对象数量,并传入数组名称,即mtexture。我们还需要通过调用glBindTexture并传入纹理类型来绑定纹理类型,即GL_TEXTURE_2D,指定它是一个 2D 纹理,并声明纹理 ID。

接下来,我们必须设置纹理包裹。纹理包裹决定了当纹理坐标在xy方向上大于或小于1时会发生什么。

纹理可以以四种方式之一进行包裹:GL_REPEATGL_MIRRORED_REPEATGL_CLAMP_TO_EDGEGL_CLAMP_TO_BORDER

如果我们想象一个纹理被应用到四边形上,那么正的s轴水平运行,而t轴垂直运行,从原点(左下角)开始,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/4d8df6ec-bef3-40c7-95b5-795dd859db2b.png

让我们看看纹理可以如何被包裹的不同方式,如下列所示:

  • GL_REPEAT 在应用于四边形时只是重复纹理。

  • GL_MIRROR_MIRROR_REPEAT 重复纹理,但下一次也会镜像纹理。

  • GL_CLAMP_TO_EDGE 将纹理边缘的 rgb 值重复应用于整个对象。在下面的截图中,红色边缘像素被重复。

  • GL_CLAMP_TO_BORDER 采用用户特定的值并将其应用于对象的末端,而不是应用边缘颜色,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/4d87c32b-008b-4a1c-8d45-e3446ca4d721.png

对于我们的目的,我们需要 GL_REPEAT,这已经是默认设置,但如果你必须设置它,你需要添加以下内容:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);  

你使用 glTexParameteri 函数,它接受三个参数。第一个是纹理类型,即 GL_TEXTURE_2D。下一个参数是你想要应用包裹方向的参数,即 STS 方向与 x 相同,Ty 相同。最后一个参数是包裹参数本身。

接下来,我们可以设置纹理过滤。有时,当你将低质量纹理应用于大四边形时,如果你放大查看,纹理将会出现像素化,如下面截图的左侧所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/9d41fb43-47e4-4d47-9ae8-495d188392d7.png

左侧的图片是设置纹理过滤为 GL_NEAREST 的输出,右侧的图片是应用纹理过滤到 GL_LINEAR 的结果。GL_LINEAR 包裹线性插值周围的纹理元素值,与 GL_NEAREST 相比,给出了更平滑的结果。

当纹理被放大时,最好将值设置为 GL_LINEAR 以获得更平滑的图像,而当图像被缩小时,可以将其设置为 GL_NEAREST,因为纹理元素(即纹理元素)将非常小,我们无论如何都看不到它们。

要设置纹理过滤,我们使用相同的 glTexParameteri 函数,但不是将包裹方向作为第二个参数传递,而是指定 GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER 作为第二个参数,并将 GL_NEARESTGL_LINEAR 作为第三个参数,如下所示:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

加载一个巨大的图像与对象如此之远以至于你甚至看不到它是没有意义的,因此出于优化的目的,你可以创建米普图。米普图基本上是将纹理转换为较低的分辨率。当纹理远离相机时,它将自动将图像转换为较低的分辨率图像。当相机更近时,它也会转换为较高的分辨率图像。

这是我们所使用纹理的米普链:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/e794d3ff-8581-4313-b14c-52eafa2f18ef.png

可以使用glTexParameteri函数再次设置米普图质量。这基本上是用GL_NEAREST替换为GL_NEAREST_MIPMAP_NEARESTGL_LINEAR_MIPMAP_NEARESTGL_NEAREST_MIPMAP_LINEARGL_LINEAR_MIPMAP_LINEAR

最佳选项是GL_LINEAR_MIPMAP_LINEAR,因为它在两个米普图中以及样本之间线性插值了纹理单元的值,同样也在周围的纹理单元之间进行线性插值(纹理单元是图像中最低的单位,就像像素是屏幕上表示颜色的最小单位一样。如果在一台 1080p 的屏幕上显示一张 1080p 的图片,那么 1 个纹理单元就映射到 1 个像素)。

因此,我们将使用以下作为我们新的过滤/米普图值:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, 
   GL_LINEAR_MIPMAP_LINEAR); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 

一旦设置完毕,我们就可以最终使用glTexImage2D函数创建纹理,如下所示:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0,GL_RGB, 
    GL_UNSIGNED_BYTE, image); 

glTexImage2D函数接受九个参数。这些参数如下所述:

  • 第一个是纹理类型,它是GL_TEXTURE_2D

  • 第二个是米普图级别。如果我们想使用较低质量的图片,可以将此值设置为123。为了我们的目的,我们将保留此值为0,这是基本级别。

  • 对于第三个参数,我们将指定我们想要从图像中存储的所有全色通道。由于我们想要存储所有三个通道,我们指定GL_RGB

  • 我们指定的第四和第五个参数是图片的宽度和高度。

  • 下一个参数必须设置为0,如文档中指定(文档可以在www.khronos.org/registry/OpenGL-Refpages/gl4/html/glTexImage2D.xhtml找到)。

  • 我们指定的下一个参数是图像源的数据格式。

  • 下一个参数是传入的数据类型,它是GL_UNSIGNED_BYTE

  • 最后,我们设置图像数据。

现在纹理已经创建,我们调用glGenerateMipmap并传入GL_TEXTURE_2D纹理类型,如下所示:

glGenerateMipmap(GL_TEXTURE_2D); 

然后,我们解绑纹理,释放图片,并最终像这样返回textureID函数:

glBindTexture(GL_TEXTURE_2D, 0); 
stbi_image_free(image); 

   return mtexture; 

所有的这些工作完成后,我们最终将我们的纹理添加到游戏对象中。

source.cpp中,通过以下步骤包含MeshRenderer.hTextureLoader.h

  1. 在顶部,创建一个名为球体的MeshRenderer指针对象,如下所示:
Camera* camera; 
LightRenderer* light; 
MeshRenderer* sphere;
  1. init函数中,创建一个新的GLuint类型的着色器程序,名为texturedShaderProgram,如下所示:
GLuint flatShaderProgram = shader.CreateProgram(
                           "Assets/Shaders/FlatModel.vs", 
                           "Assets/Shaders/FlatModel.fs"); 
GLuint texturedShaderProgram = shader.CreateProgram(
                               "Assets/Shaders/TexturedModel.vs",   
                               "Assets/Shaders/TexturedModel.fs");
  1. 我们现在将加载两个名为TexturedModel.vsTexturedModel.fs的着色器,如下所示:
  • 这里是TexturedModel.vs着色器:
#version 450 core 
layout (location = 0) in vec3 position; 
layout (location = 1) in vec2 texCoord; 

out vec2 TexCoord; 

uniform mat4 vp; 
uniform mat4 model; 

void main(){ 

   gl_Position = vp * model *vec4(position, 1.0); 

   TexCoord = texCoord; 
} 

FlatModel.vs的唯一区别是,在这里,第二个位置是一个名为texCoordvec2。我们在main函数中创建一个输出vec2,名为TexCoord,我们将在这个值中存储这个值。

  • 这里是TexturedModel.fs着色器:
 #version 450 core 

in vec2 TexCoord; 

out vec4 color; 

// texture 
uniform sampler2D Texture; 

void main(){ 

         color = texture(Texture, TexCoord);  
} 

我们创建一个新的vec2,名为TexCoord,以接收从顶点着色器传来的值。

然后,我们创建一个新的统一类型sampler2D,并命名为Texture。纹理通过一个采样器接收,该采样器将根据我们在创建纹理时设置的包装和过滤参数来采样纹理。

然后,根据采样器和纹理坐标使用texture函数设置颜色。此函数将采样器和纹理坐标作为参数。根据采样器,在纹理坐标处的 texel 被采样,并返回该颜色值,并将其分配给该纹理坐标处的对象。

让我们继续创建MeshRenderer对象。使用TextureLoader类的getTextureID函数加载globe.jpg纹理文件,并将其设置为名为sphereTextureGLuint,如下所示:

 TextureLoader tLoader; 
GLuint sphereTexture = tLoader.getTextureID("Assets/Textures/globe.jpg");  

创建球体MeshRederer对象,设置网格类型,并传递摄像机。设置程序、纹理、位置和缩放,如下所示:

   sphere = new MeshRenderer(MeshType::kSphere, camera); 
   sphere->setProgram(texturedShaderProgram); 
   sphere->setTexture(sphereTexture); 
   sphere->setPosition(glm::vec3(0.0f, 0.0f, 0.0f)); 
   sphere->setScale(glm::vec3(1.0f)); 

renderScene函数中,按照以下方式绘制sphere对象:

void renderScene(){ 

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
   glClearColor(1.0, 1.0, 0.0, 1.0); 

   sphere->draw(); 

}  

运行项目后,您应该会看到带有纹理的地球,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/9ef6e337-5eec-4eec-8aa6-9d83485789fe.png

摄像机创建如下,并将其设置为四单位的z位置:


camera = new Camera(45.0f, 800, 600, 0.1f, 100.0f, glm::vec3(0.0f, 
         0.0f, 4.0f)); 

添加 Bullet 物理

要将物理元素添加到我们的游戏中,我们将使用 Bullet 物理引擎。这是一个开源项目,在 AAA 游戏和电影中得到了广泛应用。它用于碰撞检测以及软体和刚体动力学。该库对商业用途免费。

github.com/bulletphysics/bullet3下载源代码,并使用 CMake 构建 x64 的发布版本项目。为了方便起见,该章节的项目中包含了头文件和lib文件。您可以将文件夹复制并粘贴到dependencies文件夹中。

现在我们有了文件夹,让我们看看如何按照以下步骤添加 Bullet 物理:

  1. 如下截图所示,将include文件夹添加到“C/C++ | 一般 | 额外包含目录”:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/76e2dde2-5097-4e5e-bf7f-6eed3826eaaa.png

  1. 在链接器设置中,将lib/win64/Rls文件夹添加到“链接器 | 一般 | 额外库目录”:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/51590433-e7ac-4e36-b063-09cac2f2fbb6.png

  1. BulletCollision.libBulletDynamics.libLinearMath.lib添加到“链接器 | 输入 | 额外依赖项”,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/1369c25b-a19e-4dd4-b3f7-b0f325fbf94f.png

这些库负责根据重力、外力等条件计算游戏对象的运动,进行碰撞检测和内存分配。

  1. 准备工作完成之后,我们就可以开始将物理元素添加到游戏中了。在source.cpp文件中,将btBulletDynamicsCommon.h包含在文件顶部,如下所示:
#include "Camera.h" 
#include "LightRenderer.h" 
#include "MeshRenderer.h" 
#include "TextureLoader.h" 

#include <btBulletDynamicsCommon.h> 
  1. 然后,创建一个新的指向btDiscreteDynamicsWorld的指针对象,如下所示:
btDiscreteDynamicsWorld* dynamicsWorld; 
  1. 此对象跟踪当前场景中所有物理设置和对象。

然而,在创建dynamicWorld之前,Bullet 物理库需要首先初始化一些对象。

这些必需的对象如下列出:

  • btBroadPhaseInerface:碰撞检测实际上分为两个阶段:broadphasenarrowphase。在broadphase阶段,物理引擎消除所有不太可能发生碰撞的对象。这个检查是通过使用对象的边界框来完成的。然后,在narrowphase阶段,使用对象的实际形状来检查碰撞的可能性。具有强烈碰撞可能性的对象对被创建。在以下屏幕截图中,围绕球体的红色框用于broadphase碰撞,而球体的白色线网用于narrowphase碰撞:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/f0bc4f36-35eb-4c02-b8bc-eef4a2ce1530.png

  • btDefaultColliusion configuration:这用于设置默认内存。

  • btCollisionDispatcher: 使用实际形状测试具有强烈碰撞可能性的对象对以检测碰撞。这用于获取碰撞检测的详细信息,例如哪个对象与哪个其他对象发生了碰撞。

  • btSequentialImpulseConstraintSolver:你可以创建约束,例如铰链约束或滑块约束,这些约束可以限制一个物体相对于另一个物体的运动或旋转。例如,如果墙壁和门之间存在铰链关节,那么门只能绕着关节旋转,不能移动,因为它在铰链关节处是固定的。约束求解器负责正确计算这一点。计算会重复多次,以接近最优解。

init函数中,在我们创建sphere对象之前,我们将按照以下方式初始化这些对象:

//init physics 
btBroadphaseInterface* broadphase = new btDbvtBroadphase(); 
btDefaultCollisionConfiguration* collisionConfiguration = 
   new btDefaultCollisionConfiguration(); 
btCollisionDispatcher* dispatcher = 
   new btCollisionDispatcher(collisionConfiguration); 
btSequentialImpulseConstraintSolver* solver = 
   new btSequentialImpulseConstraintSolver(); 
  1. 然后,我们将通过将dispatcherbroadphasesolvercollisionConfiguration作为参数传递给btDiscreteDynamicsWorld函数来创建一个新的dynamicWorld,如下所示:
dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, solver, collisionConfiguration); 
  1. 现在我们已经创建了物理世界,我们可以设置物理参数。基本参数是重力。我们将其值设置为现实世界的条件,如下所示:
dynamicsWorld->setGravity(btVector3(0, -9.8f, 0)); 

添加刚体

现在我们可以创建刚体或软体,并观察它们与其他刚体或软体的相互作用。刚体是一个不会改变其形状或物理特性的有生命或无生命物体。另一方面,软体可以是可挤压的,并使其形状发生变化。

在以下示例中,我们将专注于创建刚体。

要创建一个刚体,我们必须指定物体的形状和运动状态,然后设置物体的质量和惯性。形状是通过btCollisionShape定义的。一个物体可以有不同的形状,有时甚至是一个形状的组合,称为复合形状。我们使用btBoxShape来创建立方体和长方体,使用btSphereShape来创建球体。我们还可以创建其他形状,如btCapsuleShapebtCylinderShapebtConeShape,这些形状将由库用于narrowphase碰撞。

在我们的案例中,我们将创建一个球体形状并观察我们的地球球体弹跳。所以,让我们开始吧:

  1. 使用以下代码创建一个btSphere用于创建球形,并将半径设置为1.0,这也是我们渲染的球体的半径:
   btCollisionShape* sphereShape = new btSphereShape(1.0f);   
  1. 接下来,设置btDefaultMotionState,其中我们指定球体的旋转和位置,如下所示:
btDefaultMotionState* sphereMotionState = new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), btVector3(0, 10.0f, 0))); 

我们将旋转设置为0,并将刚体的位置设置为沿y轴的10.0f距离。我们还应该设置质量和惯性,并计算sphereShape的惯性,如下所示:

btScalar mass = 10.0; 
btVector3 sphereInertia(0, 0, 0); 
sphereShape->calculateLocalInertia(mass, sphereInertia); 

  1. 要创建刚体,我们首先必须创建btRigidBodyConstructionInfo并将其变量传递给它,如下所示:
btScalar mass = 10.0; 
btVector3 sphereInertia(0, 0, 0); 
sphereShape->calculateLocalInertia(mass, sphereInertia); 

btRigidBody::btRigidBodyConstructionInfo sphereRigidBodyCI(mass, 
sphereMotionState, sphereShape, sphereInertia); 

  1. 现在,通过将btRigidBodyConstructionInfo传递给它来创建刚体对象,如下所示:
btRigidBody* sphereRigidBody = new btRigidBody(sphereRigidBodyCI); 
  1. 现在,使用以下代码设置刚体的物理属性,包括摩擦和恢复力:
sphereRigidBody->setRestitution(1.0f); 
sphereRigidBody->setFriction(1.0f);  

这些值介于0.0f1.0.0.0之间,意味着物体非常光滑且没有摩擦,没有恢复力或弹性。另一方面,1.0表示物体外部粗糙且弹性极强,就像一个弹跳球。

  1. 在设置完这些必要的参数后,我们需要将刚体添加到我们创建的dynamicWorld中,如下所示,使用dynamicsWorldaddRigidBody函数:
dynamicsWorld->addRigidBody(sphereRigidBody); 

现在,为了让我们的球体网格真正像球体刚体一样表现,我们必须将刚体传递给球体网格类并做一些小的修改。打开MeshRenderer.h.cpp文件。在MeshRenderer.h文件中,包含btBulletDynamicsCommon.h头文件,并在private部分添加一个名为rigidBody的本地btRigidBody。您还应该将构造函数修改为接受一个刚体,如下所示:

#include <btBulletDynamicsCommon.h> 

   class MeshRenderer{ 

public: 
MeshRenderer(MeshType modelType, Camera* _camera, btRigidBody* _rigidBody); 
         . 
         . 
   private: 
         . 
         . 
         btRigidBody* rigidBody; 
};
  1. MeshRenderer.cpp文件中,将构造函数修改为接受一个rigidBody变量,并将局部rigidBody变量设置为它,如下所示:
MeshRenderer::MeshRenderer(MeshType modelType, Camera* _camera, btRigidBody* _rigidBody) { 

   rigidBody = _rigidBody; 
   camera = _camera; 
   . 
   . 
}
  1. 然后,在draw函数中,我们必须替换设置modelMatrix变量的代码,使用获取球体刚体值的代码,如下所示:
   btTransform t; 

   rigidBody->getMotionState()->getWorldTransform(t); 
  1. 我们使用btTransform变量从刚体的getMotionState函数中获取变换,然后获取WorldTransform变量并将其设置为我们的brTransform变量t,如下所示:
   btQuaternion rotation = t.getRotation(); 
   btVector3 translate = t.getOrigin(); 
  1. 我们创建两个新的 btQuaternion 类型的变量来存储旋转,以及一个 btVector3 类型的变量来存储变换值,使用 btTransform 类的 getRotationgetOrigin 函数,如下所示:
glm::mat4 RotationMatrix = glm::rotate(glm::mat4(1.0f), rotation.getAngle(),glm::vec3(rotation.getAxis().getX(),rotation.getAxis().getY(), rotation.getAxis().getZ())); 

glm::mat4 TranslationMatrix = glm::translate(glm::mat4(1.0f), 
                              glm::vec3(translate.getX(),  
                              translate.getY(), translate.getZ())); 

glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0f), scale); 
  1. 接下来,我们创建三个 glm::mat4 类型的变量,分别称为 RotationMatrixTranslationMatrixScaleMatrix,并使用 glm::rotateglm::translation 函数设置旋转和变换的值。然后,我们将之前存储的旋转和变换值传递进去,如下所示。我们将保持 ScaleMatrix 变量不变:
   modelMatrix = TranslationMatrix * RotationMatrix * scaleMatrix;  

新的 modelMatrix 变量将是按照顺序缩放、旋转和变换矩阵的乘积。在 draw 函数中,其余的代码将保持不变。

  1. init 函数中,更改代码以反映修改后的 MeshRenderer 构造函数:
   // Sphere Mesh 

   sphere = new MeshRenderer(MeshType::kSphere, camera, 
            sphereRigidBody); 
   sphere->setProgram(texturedShaderProgram); 
   sphere->setTexture(sphereTexture); 
   sphere->setScale(glm::vec3(1.0f)); 
  1. 我们不需要设置位置,因为这将由刚体设置。按照以下代码设置相机,以便我们可以看到球体:
camera = new Camera(45.0f, 800, 600, 0.1f, 100.0f, glm::vec3(0.0f, 
         4.0f, 20.0f)); 
  1. 现在,运行项目。我们可以看到球体正在被绘制,但它没有移动。这是因为我们必须更新物理体。

  2. 我们必须使用 dynamicsWorldstepSimulation 函数来每帧更新模拟。为此,我们必须计算前一个帧和当前帧之间的时间差。

  3. source.cpp 的顶部包含 <chrono>,这样我们就可以计算 tick 更新。现在,我们必须对 main 函数和 while 循环进行如下更改:

auto previousTime = std::chrono::high_resolution_clock::now(); 

while (!glfwWindowShouldClose(window)){ 

         auto currentTime = std::chrono::
                            high_resolution_clock::now(); 
         float dt = std::chrono::duration<float, std::
                    chrono::seconds::period>(currentTime - 
                    previousTime).count(); 

         dynamicsWorld->stepSimulation(dt); 

         renderScene(); 

         glfwSwapBuffers(window); 
         glfwPollEvents(); 

         previousTime = currentTime; 
   } 

while 循环之前,我们创建一个名为 previousTime 的变量,并用当前时间初始化它。在 while 循环中,我们获取当前时间并将其存储在变量中。然后,我们通过减去两个时间来计算前一个时间和当前时间之间的时间差。现在我们有了时间差,所以我们调用 stepSimulation 并传入时间差。然后我们渲染场景,交换缓冲区并轮询事件,就像平常一样。最后,我们将当前时间设置为前一个时间。

现在,当我们运行项目时,我们可以看到球体正在下落,这非常酷。然而,球体没有与任何东西互动。

让我们在底部添加一个盒子刚体,并观察球体如何从它弹起。在球体 MeshRenderer 对象之后,添加以下代码来创建一个盒子刚体:

   btCollisionShape* groundShape = new btBoxShape(btVector3(4.0f, 
                                   0.5f, 4.0f)); 

   btDefaultMotionState* groundMotionState = new  
     btDefaultMotionState(btTransform(btQuaternion
     (0, 0, 0, 1), btVector3(0, -2.0f, 0))); 
   btRigidBody::btRigidBodyConstructionInfo 
    groundRigidBodyCI(0.0f, new btDefaultMotionState(), 
    groundShape, btVector3(0, 0, 0)); 

   btRigidBody* groundRigidBody = new btRigidBody(
                                  groundRigidBodyCI); 

   groundRigidBody->setFriction(1.0); 
   groundRigidBody->setRestitution(0.9); 

   groundRigidBody->setCollisionFlags(btCollisionObject
     ::CF_STATIC_OBJECT); 

   dynamicsWorld->addRigidBody(groundRigidBody);  

在这里,我们首先创建一个 btBoxShape 类型的形状,长度、高度和深度分别设置为 4.00.54.0。接下来,我们将设置运动状态,其中我们将旋转设置为零,并将位置设置为 y 轴上的 -2.0x 轴和 z 轴上的 0。对于构造信息,我们将质量和惯性设置为 0。我们还设置了默认的运动状态并将形状传入。接下来,我们通过将刚体信息传入其中来创建刚体。一旦创建了刚体,我们就设置了恢复力和摩擦值。接下来,我们使用 rigidBodysetCollisionFlags 函数将刚体类型设置为静态。这意味着它将像砖墙一样,不会移动并且不受其他刚体作用力的影响,但其他物体仍然会受到它的影响。

最后,我们将地面刚体添加到世界中,这样盒子刚体也将成为物理模拟的一部分。我们现在必须创建一个用于渲染地面刚体的 MeshRenderer 立方体。在顶部创建一个新的 MeshRenderer 对象,称为 Ground,在其下方你创建了球体 MeshRenderer 对象。在 init 函数中,我们在其中添加了地面刚体的代码,添加以下内容:

   // Ground Mesh 
   GLuint groundTexture = tLoader.getTextureID(
                          "Assets/Textures/ground.jpg"); 
   ground = new MeshRenderer(MeshType::kCube, camera,  
            groundRigidBody); 
   ground->setProgram(texturedShaderProgram); 
   ground->setTexture(groundTexture); 
   ground->setScale(glm::vec3(4.0f, 0.5f, 4.0f));  

我们将通过加载 ground.jpg 创建一个新的纹理,所以请确保你已经将它添加到 Assets/ Textures 目录中。调用构造函数并将 meshtype 设置为 cube,然后设置相机并传入地面刚体。接下来,我们设置着色器程序、纹理和物体的比例。

  1. renderScene 函数中,按照以下方式绘制地面 MeshRenderer 对象:
void renderScene(){ 

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
   glClearColor(1.0, 1.0, 0.0, 1.0); 

   sphere->draw(); 
   ground->draw(); 
}
  1. 现在,当你运行项目时,你将看到球体在地面上弹跳:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/5aa9e70f-6bc0-4e07-8212-24d7cebc2dbf.png

摘要

在本章中,我们创建了一个名为 MeshRenderer 的新类,它将被用来将纹理化的 3D 对象渲染到场景中。我们创建了一个纹理加载类,它将被用来从提供的图像中加载纹理。然后,我们通过添加 Bullet Physics 库给对象添加了物理效果。然后我们初始化了物理世界,并通过将刚体本身添加到世界中,创建了并添加了刚体到网格渲染器中,使得渲染的物体受到物理影响。

在下一章中,我们将添加游戏循环,以及计分和文本渲染来在视口中显示分数。我们还将向我们的世界添加光照。

第八章:通过碰撞、循环和光照增强您的游戏

在本章中,我们将学习如何添加碰撞以检测球和敌人之间的接触;这将确定失败条件。我们还将检查球和地面之间的接触,以确定玩家是否可以跳跃。然后,我们将完成游戏循环。

一旦游戏循环完成,我们就能添加文本渲染来显示玩家的得分。为了显示必要的文本,我们将使用 FreeType 库。这将从字体文件中加载字符。

我们还将向场景中的对象添加一些基本光照。光照将使用 Phong 光照模型进行计算,我们将介绍如何在实践中实现这一点。为了完成游戏循环,我们必须添加一个敌人。

在本章中,我们将涵盖以下主题:

  • 添加RigidBody名称

  • 添加敌人

  • 移动敌人

  • 检查碰撞

  • 添加键盘控制

  • 游戏循环和得分

  • 文本渲染

  • 添加光照

添加刚体名称

为了识别我们将要添加到场景中的不同刚体,我们将在MeshRenderer类中添加一个属性,该属性将指定每个被渲染的对象。让我们看看如何做到这一点:

  1. MeshRenderer.h类中,该类位于MeshRenderer类内部,将类的构造函数修改为接受一个字符串作为对象的名称,如下所示:
MeshRenderer(MeshType modelType, std::string _name, Camera *  
   _camera, btRigidBody* _rigidBody) 
  1. 添加一个名为name的新公共属性,其类型为std::string,并初始化它,如下所示:
         std::string name = ""; 
  1. 接下来,在MeshRenderer.cpp文件中,修改构造函数的实现,如下所示:
MeshRenderer::MeshRenderer(MeshType modelType, std::string _name,  
   Camera* _camera, btRigidBody* _rigidBody){ 

   name = _name; 
... 
... 

} 

我们已成功将name属性添加到MeshRenderer类中。

添加敌人

在我们将敌人添加到场景之前,让我们稍微整理一下代码,并在main.cpp中创建一个名为addRigidBodies的新函数,以便所有刚体都在一个函数中创建。为此,请按照以下步骤操作:

  1. main.cpp文件的源代码中,在main()函数上方创建一个名为addRigidBodies的新函数。

  2. 将以下代码添加到addRigidBodies函数中。这将添加球体和地面。我们这样做是为了避免将所有游戏代码放入main()函数中:

   // Sphere Rigid Body 

   btCollisionShape* sphereShape = new btSphereShape(1); 
   btDefaultMotionState* sphereMotionState = new 
     btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), 
     btVector3(0, 0.5, 0))); 

   btScalar mass = 13.0f; 
   btVector3 sphereInertia(0, 0, 0); 
   sphereShape->calculateLocalInertia(mass, sphereInertia); 

   btRigidBody::btRigidBodyConstructionInfo sphereRigidBodyCI(mass, 
      sphereMotionState, sphereShape, sphereInertia); 

   btRigidBody* sphereRigidBody = new btRigidBody(
                                  sphereRigidBodyCI); 

   sphereRigidBody->setFriction(1.0f); 
   sphereRigidBody->setRestitution(0.0f); 

   sphereRigidBody->setActivationState(DISABLE_DEACTIVATION); 

   dynamicsWorld->addRigidBody(sphereRigidBody); 

   // Sphere Mesh 

   sphere = new MeshRenderer(MeshType::kSphere, "hero", camera, 
            sphereRigidBody); 
   sphere->setProgram(texturedShaderProgram); 
   sphere->setTexture(sphereTexture); 
   sphere->setScale(glm::vec3(1.0f)); 

   sphereRigidBody->setUserPointer(sphere); 

   // Ground Rigid body 

   btCollisionShape* groundShape = new btBoxShape(btVector3(4.0f, 
                                   0.5f, 4.0f)); 
   btDefaultMotionState* groundMotionState = new 
       btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), 
       btVector3(0, -1.0f, 0))); 

   btRigidBody::btRigidBodyConstructionInfo groundRigidBodyCI(0.0f, 
      groundMotionState, groundShape, btVector3(0, 0, 0)); 

   btRigidBody* groundRigidBody = new btRigidBody(
                                  groundRigidBodyCI); 

   groundRigidBody->setFriction(1.0); 
   groundRigidBody->setRestitution(0.0); 

   groundRigidBody->setCollisionFlags(
       btCollisionObject::CF_STATIC_OBJECT); 

   dynamicsWorld->addRigidBody(groundRigidBody); 

   // Ground Mesh 
   ground = new MeshRenderer(MeshType::kCube, "ground", camera, 
            groundRigidBody); 
   ground->setProgram(texturedShaderProgram); 
   ground->setTexture(groundTexture); 
   ground->setScale(glm::vec3(4.0f, 0.5f, 4.0f)); 

   groundRigidBody->setUserPointer(ground); 

注意,一些值已被更改以适应我们的游戏。我们还将禁用球体的去激活,因为我们不这样做的话,当我们需要球体为我们跳跃时,球体将无响应。

要访问渲染网格的名称,我们可以通过使用RigidBody类的setUserPointer属性将该实例设置为刚体的一个属性。setUserPointer接受一个 void 指针,因此可以传递任何类型的数据。为了方便起见,我们只是传递MeshRenderer类的实例本身。在这个函数中,我们还将添加敌人的刚体到场景中,如下所示:

// Enemy Rigid body 

btCollisionShape* shape = new btBoxShape(btVector3(1.0f, 1.0f, 1.0f)); 
btDefaultMotionState* motionState = new btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), 
btVector3(18.0, 1.0f, 0))); 
btRigidBody::btRigidBodyConstructionInfo rbCI(0.0f, motionState, shape, btVector3(0.0f, 0.0f, 0.0f)); 

   btRigidBody* rb = new btRigidBody(rbCI); 

   rb->setFriction(1.0); 
   rb->setRestitution(0.0); 

//rb->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT); 

rb->setCollisionFlags(btCollisionObject::CF_NO_CONTACT_RESPONSE); 

   dynamicsWorld->addRigidBody(rb); 

   // Enemy Mesh 
   enemy = new MeshRenderer(MeshType::kCube, "enemy", camera, rb); 
   enemy->setProgram(texturedShaderProgram); 
   enemy->setTexture(groundTexture); 
   enemy->setScale(glm::vec3(1.0f, 1.0f, 1.0f)); 

   rb->setUserPointer(enemy); 
  1. 以与我们添加球体和地面的相同方式添加敌人。由于敌人对象的形状是立方体,我们使用btBoxShape为刚体设置盒子的形状。我们将位置设置为沿X轴 18 个单位距离和沿Y轴 1 个单位距离。然后,我们设置摩擦和恢复值。

对于刚体的类型,我们将它的碰撞标志设置为NO_CONTACT_RESPONSE而不是KINEMATIC_OBJECT。我们本来可以将类型设置为KINEMATIC_OBJECT,但那样的话,当敌人对象与之接触时,它会对其他对象,如球体,施加力。为了避免这种情况,我们使用NO_CONTACT_RESPONSE,它只会检查敌人刚体和另一个物体之间是否有重叠,而不是对其施加力。

您可以取消注释KINEMATIC_OBJECT代码行的注释,并注释掉NO_CONTACT_RESPONSE代码行,以查看使用任一方式如何改变物体在物理模拟中的行为。

  1. 一旦我们创建了刚体,我们将刚体添加到世界中,为敌人对象设置网格渲染器,并将其命名为敌人

移动敌人

为了更新敌人的移动,我们将添加一个由刚体世界调用的tick函数。在这个tick函数中,我们将更新敌人的位置,使其从屏幕的右侧移动到左侧。我们还将检查敌人是否已经超过了屏幕的左侧边界。

如果已经超过,那么我们将将其位置重置为屏幕的右侧。为此,请按照以下步骤操作:

  1. 在这个更新函数中,我们将更新我们的游戏逻辑和得分,以及我们如何检查球体与敌人以及球体与地面的接触。将tick函数回调原型添加到Main.cpp文件的顶部,如下所示:
   void myTickCallback(btDynamicsWorld *dynamicsWorld, 
      btScalar timeStep); 
  1. TickCallback函数中更新敌人的位置,如下所示:
void myTickCallback(btDynamicsWorld *dynamicsWorld, btScalar timeStep) { 

         // Get enemy transform 
         btTransform t(enemy->rigidBody->getWorldTransform()); 

         // Set enemy position 
         t.setOrigin(t.getOrigin() + btVector3(-15, 0, 0) * 
         timeStep); 

         // Check if offScreen 
         if(t.getOrigin().x() <= -18.0f) { 
               t.setOrigin(btVector3(18, 1, 0)); 
         } 
         enemy->rigidBody->setWorldTransform(t); 
         enemy->rigidBody->getMotionState()->setWorldTransform(t); 

} 

myTickCallback函数中,我们获取当前的变换并将其存储在一个变量t中。然后,我们通过获取当前位置,将其向左移动 15 个单位,并将其乘以当前时间步长(即前一个时间和当前时间之间的差异)来设置原点,即变换的位置。

一旦我们得到更新后的位置,我们检查当前位置是否小于 18 个单位。如果是,那么当前位置已经超出了屏幕左侧的边界。因此,我们将当前位置设置回视口的右侧,并使物体在屏幕上环绕。

然后,我们通过更新刚体的worldTransform和物体的运动状态来更新物体本身的位置到这个新位置。

  1. init函数中将tick函数设置为动态世界的默认TickCallback,如下所示:
dynamicsWorld = new btDiscreteDynamicsWorld(dispatcher, broadphase, 
                solver, collisionConfiguration); 
dynamicsWorld->setGravity(btVector3(0, -9.8f, 0));  
dynamicsWorld->setInternalTickCallback(myTickCallback); 
  1. 构建并运行项目,以查看屏幕右侧生成的立方体敌人,然后它穿过球体并向屏幕左侧移动。当敌人离开屏幕时,它将循环回到屏幕右侧,如下面的截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/ec234c82-a108-4331-bfa9-e8c8fe7eab04.png

  1. 如果我们将敌人的collisionFlag设置为KINEMATIC_OBJECT,你会看到敌人不会穿过球体,而是将其推离地面,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/ab66fea4-7ddd-4c3d-b00c-caf95c27b182.png

  1. 这不是我们想要的,因为我们不希望敌人与任何对象进行物理交互。将敌人的碰撞标志改回NO_CONTACT_RESPONSE以修正此问题。

检查碰撞

在 tick 函数中,我们需要检查球体与敌人以及球体与地面的碰撞。按照以下步骤进行:

  1. 要检查对象之间的接触数,我们将使用动态世界对象的getNumManifolds属性。在每次更新周期中,流形将包含有关场景中所有接触的信息。

  2. 我们需要检查联系人数是否大于零。如果是,那么我们检查哪些对象对彼此接触。在更新敌人对象后,添加以下代码以检查英雄与敌人之间的接触:

int numManifolds = dynamicsWorld->getDispatcher()->
  getNumManifolds(); 

   for (int i = 0; i < numManifolds; i++) { 

       btPersistentManifold *contactManifold = dynamicsWorld->
       getDispatcher()->getManifoldByIndexInternal(i); 

       int numContacts = contactManifold->getNumContacts(); 

       if (numContacts > 0) { 

           const btCollisionObject *objA = contactManifold->
           getBody0(); 
           const btCollisionObject *objB = contactManifold->
           getBody1(); 

           MeshRenderer* gModA = (MeshRenderer*)objA->
           getUserPointer(); 
           MeshRenderer* gModB = (MeshRenderer*)objB->
           getUserPointer(); 

                if ((gModA->name == "hero" && gModB->name == 
                  "enemy") || (gModA->name == "enemy" && gModB->
                  name == "hero")) { 
                        printf("collision: %s with %s \n",
                        gModA->name, gModB->name); 

                         if (gModB->name == "enemy") { 
                             btTransform b(gModB->rigidBody-
                             >getWorldTransform()); 
                             b.setOrigin(btVector3(18, 1, 0)); 
                             gModB->rigidBody-
                             >setWorldTransform(b); 
                             gModB->rigidBody-> 
                             getMotionState()-
                             >setWorldTransform(b); 
                           }else { 

                                 btTransform a(gModA->rigidBody->
                                 getWorldTransform()); 
                                 a.setOrigin(btVector3(18, 1, 0)); 
                                 gModA->rigidBody->
                                 setWorldTransform(a); 
                                 gModA->rigidBody->
                                 getMotionState()->
                                 setWorldTransform(a); 
                           } 

                     } 

                     if ((gModA->name == "hero" && gModB->name == 
                         "ground") || (gModA->name == "ground" &&               
                          gModB->name  == "hero")) { 
                           printf("collision: %s with %s \n",
                           gModA->name, gModB->name); 

                     } 
         } 
   } 
  1. 首先,我们获取接触流形或接触对的数量。然后,对于每个接触流形,我们检查接触数是否大于零。如果是大于零,那么这意味着在当前更新中已经发生了接触。

  2. 然后,我们获取两个碰撞对象并将它们分配给ObjAObjB。之后,我们获取两个对象的用户指针并将它们转换为MeshRenderer类型,以访问我们分配的对象的名称。在检查两个对象之间的接触时,对象 A 可以与对象 B 接触,或者反之亦然。如果球体与敌人之间有接触,我们将敌人位置设置回视口的右侧。我们还检查球体与地面的接触。如果有接触,我们只需打印出有接触即可。

添加键盘控制

让我们添加一些键盘控制,以便我们可以与球体交互。我们将设置,当我们按下键盘上的上键时,球体会跳跃。我们将通过向球体应用冲量来添加跳跃功能。为此,请按照以下步骤操作:

  1. 首先,我们将使用GLFW,它有一个键盘回调函数,这样我们就可以为游戏添加键盘交互。在我们开始main()函数之前,我们将设置此键盘回调函数:
void updateKeyboard(GLFWwindow* window, int key, int scancode, int action, int mods){ 

   if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { 
         glfwSetWindowShouldClose(window, true);    
   } 

   if (key == GLFW_KEY_UP && action == GLFW_PRESS) { 
               if (grounded == true) { 
                     grounded = false; 

sphere->rigidBody->applyImpulse(btVector3(0.0f, 
   100.0f, 0.0f), btVector3(0.0f, 0.0f, 0.0f)); 
                     printf("pressed up key \n"); 
               } 
         } 
} 

我们关注的两个主要参数是键和动作。通过键,我们可以获取哪个键被按下,通过动作,我们可以检索对该键执行了什么操作。在函数中,我们使用glfwGetKey函数检查是否按下了Esc键。如果是,则使用glfwSetWindowShouldClose函数通过传递true作为第二个参数来关闭窗口。

要使球体跳跃,我们检查是否按下了向上键。如果是,我们创建一个新的布尔成员变量grounded,它描述了球体接触地面时的状态。如果是真的,我们将布尔值设置为false,并通过调用rigidbodyapplyImpulse函数在 Y 方向上对球体的刚体原点施加100单位的冲量。

  1. tick函数中,在我们获取流形数量之前,我们将grounded布尔值设置为false,如下所示:
 grounded = false; 

   int numManifolds = dynamicsWorld->getDispatcher()->
                      getNumManifolds(); 
  1. 当球体和地面接触时,我们将grounded布尔值设置为true,如下所示:
   if ((gModA->name == "hero" && gModB->name == "ground") || 
         (gModA->name == "ground" && gModB->name == "hero")) { 

//printf("collision: %s with %s \n", gModA->name, gModB->name); 

         grounded = true; 

   }   
  1. 在主函数中,使用glfwSetKeyCallbackupdateKeyboard设置为回调,如下所示:
int main(int argc, char **argv) { 
...       
   glfwMakeContextCurrent(window); 
   glfwSetKeyCallback(window, updateKeyboard); 
   ... 
   }
  1. 现在,构建并运行应用程序。按下向上键以查看球体跳跃,但只有当它接触地面时,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/e9392898-a6a3-4ddf-ae97-46d496b357c3.png

游戏循环和计分

让我们通过添加计分和完成游戏循环来结束这个话题:

  1. 除了grounded布尔值外,再添加另一个布尔值并检查gameover。完成这些后,在main.cpp文件顶部添加一个名为scoreint,并将其初始化为0,如下所示:
GLuint sphereTexture, groundTexture; 

bool grounded = false; 
bool gameover = true; 
int score = 0; 

  1. 接下来,在tick函数中,敌人只有在游戏未结束时才会移动。因此,我们将敌人的位置更新包裹在一个if语句中,以检查游戏是否结束。如果游戏未结束,则更新敌人的位置,如下所示:
void myTickCallback(btDynamicsWorld *dynamicsWorld, btScalar timeStep) { 

   if (!gameover) { 

         // Get enemy transform 
         btTransform t(enemy->rigidBody->getWorldTransform()); 

         // Set enemy position 

         t.setOrigin(t.getOrigin() + btVector3(-15, 0, 0) * 
         timeStep); 

         // Check if offScreen 

         if (t.getOrigin().x() <= -18.0f) { 

               t.setOrigin(btVector3(18, 1, 0)); 
               score++; 
               label->setText("Score: " + std::to_string(score)); 

         } 

         enemy->rigidBody->setWorldTransform(t); 
         enemy->rigidBody->getMotionState()->setWorldTransform(t); 
   } 
... 
} 
  1. 如果敌人超出屏幕的左侧,我们也增加分数。仍然在tick函数中,如果球体和敌人之间有接触,我们将分数设置为0并将gameover设置为true,如下所示:

         if ((gModA->name == "hero" && gModB->name == "enemy") || 
                    (gModA->name == "enemy" && gModB->name ==
                     "hero")) { 

                     if (gModB->name == "enemy") { 
                         btTransform b(gModB->rigidBody->
                         getWorldTransform()); 
                         b.setOrigin(btVector3(18, 1, 0)); 
                         gModB->rigidBody->
                         setWorldTransform(b); 
                         gModB->rigidBody->getMotionState()->
                         setWorldTransform(b); 
                           }else { 

                           btTransform a(gModA->rigidBody->
                           getWorldTransform()); 
                           a.setOrigin(btVector3(18, 1, 0)); 
                           gModA->rigidBody->
                           setWorldTransform(a); 
                           gModA->rigidBody->getMotionState()->
                           setWorldTransform(a); 
                           } 

                           gameover = true; 
                           score = 0; 

                     }   
  1. updateKeyboard函数中,当按下向上键盘键时,我们检查游戏是否结束。如果是,我们将gameover布尔值设置为false,这将开始游戏。现在,当玩家再次按下向上键时,角色将跳跃。这样,相同的键可以用来开始游戏,也可以用来使角色跳跃。

  2. 根据以下要求修改updateKeyboard函数,如下所示:

void updateKeyboard(GLFWwindow* window, int key, int scancode, int action, int mods){ 

   if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) { 
         glfwSetWindowShouldClose(window, true); 
   } 

   if (key == GLFW_KEY_UP && action == GLFW_PRESS) { 

         if (gameover) { 
               gameover = false; 
         } else { 

               if (grounded == true) { 

                     grounded = false; 

sphere->rigidBody->applyImpulse(btVector3(0.0f, 100.0f, 0.0f), 
   btVector3(0.0f, 0.0f, 0.0f)); 
                     printf("pressed up key \n"); 
               } 
         } 
   } 
}   
  1. 虽然我们在计算分数,但用户仍然看不到分数是多少,所以让我们给游戏添加文本渲染。

文本渲染

对于文本渲染,我们将使用一个名为 FreeType 的库,加载字体并从中读取字符。FreeType 可以加载一个流行的字体格式,称为 TrueType。TrueType 字体具有.ttf扩展名。

TTFs 包含称为 glyphs 的矢量信息,可以用来存储任何数据。一个用例当然是使用它们来表示字符。

因此,当我们想要渲染特定的字形时,我们将通过指定其大小来加载字符字形;字符将以不损失质量的方式生成。

FreeType 库的源代码可以从他们的网站 www.freetype.org/ 下载,并从中构建库。也可以从 github.com/ubawurinna/freetype-windows-binaries 下载预编译的库。

让我们将库添加到我们的项目中。由于我们正在为 64 位操作系统开发,我们感兴趣的是 include 目录和 win64 目录;它们包含我们项目版本的 freetype.libfreetype.dll 文件:

  1. 在你的依赖文件夹中创建一个名为 freetype 的文件夹,并将文件提取到其中,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/26c04855-bd22-4ccd-a0b7-9ba34233dcff.png

  1. 打开项目的属性,在 C/C++ 下的 Additional Include Directory(附加包含目录)中添加 freetype 包含目录的位置,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/57339f31-2a05-43dc-aeaf-9155d6d2a478.png

  1. 在 Configuration Properties | Linker | General | Additional Library Directories(配置属性 | 链接器 | 一般 | 附加库目录)下,添加 freetype win64 目录,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/c02482b4-37c3-4a6d-b849-a0bd9501f4dc.png

  1. 在项目目录中,从 win64 目录复制 Freetype.dll 文件并将其粘贴到这里:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/2c29c11e-af9c-4aa1-a429-2e7802f3d41f.png

在准备工作完成之后,我们可以开始着手进行项目工作了。

  1. 创建一个名为 TextRenderer 的类,以及一个名为 TextRenderer.h 的文件和一个名为 TextRenderer.cpp 的文件。我们将向这些文件添加文本渲染的功能。在 TextRenderer.h 中,包含 GLglm 的常用包含头文件,如下所示:
#include <GL/glew.h> 

#include "Dependencies/glm/glm/glm.hpp" 
#include "Dependencies/glm/glm/gtc/matrix_transform.hpp" 
#include "Dependencies/glm/glm/gtc/type_ptr.hpp"
  1. 接下来,我们将包含 freetype.h 的头文件,如下所示:
#include <ft2build.h> 
#include FT_FREETYPE_H    
  1. FT_FREETYPE_H 宏仅仅在 freetype 目录中包含了 freetype.h。然后,我们将 include <map>,因为我们需要映射每个字符的位置、大小和其他信息。我们还将 include <string> 并将一个字符串传递给要渲染的类,如下所示:
#include <string> 
  1. 对于每个字形,我们需要跟踪某些属性。为此,我们将创建一个名为 Characterstruct,如下所示:
struct Character { 
   GLuint     TextureID;  // Texture ID of each glyph texture 
   glm::ivec2 Size;       // glyph Size 
   glm::ivec2 Bearing;    // baseline to left/top of glyph 
   GLuint     Advance;    // id to next glyph 
}; 

对于每个字形,我们将存储我们为每个字符创建的纹理的纹理 ID。我们存储它的大小、基线,即字形顶部左角到字形基线的距离,以及字体文件中下一个字形的 ID。

  1. 这就是包含所有字符字形的字体文件的外观:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/1f02801f-784a-4cca-9cb5-1af8ca0ac180.png

每个字符的信息都是相对于其相邻字符存储的,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/c492e9ab-55b9-45ab-97b1-736e587acda3.png

在加载 FT_Face 类型的字体面之后,我们可以逐个访问这些属性。每个字形的宽度和高度可以通过每个字形的属性访问,即 face->glyph->bitmap.widthface->glyph->bitmap.rows

每个字形的图像数据可以通过 bitmap.buffer 属性访问,我们在创建每个字形的纹理时将使用它。以下代码显示了所有这些是如何实现的。

如果字体是水平对齐的,可以通过字形的 advance.x 属性访问字体文件中的下一个字形。

关于库的理论就到这里。如果你有兴趣了解更多,必要的文档可以在 FreeType 的网站上找到:www.freetype.org/freetype2/docs/tutorial/step2.html#section-1

让我们继续处理 TextRenderer.h 文件,并创建 TextRenderer 类,如下所示:

class TextRenderer{ 

public: 
   TextRenderer(std::string text, std::string font, int size, 
     glm::vec3 color, GLuint  program); 
   ~TextRenderer(); 

   void draw(); 
   void setPosition(glm::vec2 _position); 
   void setText(std::string _text); 

private: 
   std::string text; 
   GLfloat scale; 
   glm::vec3 color; 
   glm::vec2 position; 

   GLuint VAO, VBO, program; 
   std::map<GLchar, Character> Characters; 

};   

在公共部分下的类中,我们添加构造函数和析构函数。在构造函数中,我们传入要绘制的字符串、要使用的文件、要绘制的文本的大小和颜色,以及传入在绘制字体时使用的着色器程序。

然后,我们有 draw 函数来绘制文本,几个设置器来设置位置,以及一个 setText 函数,如果需要,可以设置新的字符串进行绘制。在私有部分,我们有用于文本字符串、缩放、颜色和位置的局部变量。我们还有 VAOVBOprogram 的成员变量,这样我们就可以绘制文本字符串。在类的末尾,我们创建一个映射来存储所有加载的字符,并将每个 GLchar 分配到映射中的字符 struct。这就是 TextRenderer.h 文件需要做的所有事情。

TextRenderer.cpp 文件中,将 TextRenderer.h 文件包含在文件顶部,并执行以下步骤:

  1. 添加 TextRenderer 构造函数的实现,如下所示:
TextRenderer::TextRenderer(std::string text, std::string font, int size, glm::vec3 color, GLuint program){ 

} 

在构造函数中,我们将添加加载所有字符的功能,并为绘制文本准备类。

  1. 让我们初始化局部变量,如下所示:
   this->text = text; 
   this->color = color; 
   this->scale = 1.0; 
   this->program = program; 
   this->setPosition(position); 
  1. 接下来,我们需要设置投影矩阵。对于文本,我们指定正交投影,因为它没有深度,如下所示:
   glm::mat4 projection = glm::ortho(0.0f, static_cast<GLfloat>
                         (800), 0.0f, static_cast<GLfloat>(600)); 
   glUseProgram(program); 
   glUniformMatrix4fv(glGetUniformLocation(program, "projection"), 
      1, GL_FALSE, glm::value_ptr(projection)); 

投影是通过 glm::ortho 函数创建的,它接受原点 x、窗口宽度、原点 y 和窗口高度作为创建正交投影矩阵的参数。我们将使用当前程序并将投影矩阵的值传递到名为 projection 的位置,然后将其传递给着色器。由于这个值永远不会改变,它将在构造函数中调用并赋值一次。

  1. 在加载字体本身之前,我们必须初始化 FreeType 库,如下所示:
// FreeType 
FT_Library ft; 

// Initialise freetype 
if (FT_Init_FreeType(&ft)) 
std::cout << "ERROR::FREETYPE: Could not init FreeType Library" 
          << std::endl; 
  1. 现在,我们可以加载字体本身,如下所示:
// Load font 
FT_Face face; 
if (FT_New_Face(ft, font.c_str(), 0, &face)) 
         std::cout << "ERROR::FREETYPE: Failed to load font" 
                   << std::endl; 

  1. 现在,设置字体大小(以像素为单位)并禁用字节对齐限制。如果我们不对字节对齐进行限制,字体将被绘制得混乱,所以别忘了添加这个:
// Set size of glyphs 
FT_Set_Pixel_Sizes(face, 0, size); 

// Disable byte-alignment restriction 
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 
  1. 然后,我们将加载我们加载的字体中的前128个字符,并创建和分配纹理 ID、大小、基线和进位。之后,我们将字体存储在字符映射中,如下所示:
   for (GLubyte i = 0; i < 128; i++){ 

         // Load character glyph  
         if (FT_Load_Char(face, i, FT_LOAD_RENDER)){ 
               std::cout << "ERROR::FREETYTPE: Failed to 
                            load Glyph" << std::endl; 
               continue; 
         } 

         // Generate texture 
         GLuint texture; 
         glGenTextures(1, &texture); 
         glBindTexture(GL_TEXTURE_2D, texture); 

         glTexImage2D( 
               GL_TEXTURE_2D, 
               0, 
               GL_RED, 
               face->glyph->bitmap.width, 
               face->glyph->bitmap.rows, 
               0, 
               GL_RED, 
               GL_UNSIGNED_BYTE, 
               face->glyph->bitmap.buffer 
               ); 

         // Set texture filtering options 
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, 
         GL_CLAMP_TO_EDGE); 
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, 
         GL_CLAMP_TO_EDGE); 
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
         GL_LINEAR); 
         glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER,
         GL_LINEAR); 

         // Create a character 
         Character character = { 
               texture, 
               glm::ivec2(face->glyph->bitmap.width, 
                           face->glyph->bitmap.rows), 
               glm::ivec2(face->glyph->bitmap_left, 
           face->glyph->bitmap_top), 
               face->glyph->advance.x 
         }; 

         // Store character in characters map 
         Characters.insert(std::pair<GLchar, Character>(i,
         character)); 
   } 
  1. 一旦加载了字符,我们可以解绑纹理并销毁字体外观和 FreeType 库,如下所示:
   glBindTexture(GL_TEXTURE_2D, 0); 

   // Destroy FreeType once we're finished 
   FT_Done_Face(face); 
   FT_Done_FreeType(ft);
  1. 每个字符都将作为一个单独的四边形上的纹理来绘制,因此为四边形设置VAO/VBO,创建一个位置属性并启用它,如下所示:
   glGenVertexArrays(1, &VAO); 
   glGenBuffers(1, &VBO); 

   glBindVertexArray(VAO); 

   glBindBuffer(GL_ARRAY_BUFFER, VBO); 
   glBufferData(GL_ARRAY_BUFFER, sizeof(GLfloat) * 6 * 4, NULL, 
       GL_DYNAMIC_DRAW); 

   glEnableVertexAttribArray(0); 
   glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * 
      sizeof(GLfloat), 0); 

  1. 现在,我们需要解绑VBOVAO,如下所示:
   glBindBuffer(GL_ARRAY_BUFFER, 0); 
   glBindVertexArray(0); 

构造函数就到这里。现在,我们可以继续到绘制函数。让我们看看:

  1. 首先,创建绘制函数的实现,如下所示:
void TextRenderer::draw(){
} 
  1. 我们将向这个函数添加绘制功能。首先,我们将获取文本需要开始绘制的位置,如下所示:
glm::vec2 textPos = this->position; 
  1. 然后,我们必须启用混合。如果我们不启用混合,整个文本的四边形将被着色,而不是仅着色文本存在的区域,如左边的图像所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/4b5c4d13-726e-4ea5-b2cb-b6f3c6784d9b.png

在左边的图像中,S 应该出现的地方,我们可以看到整个四边形被红色着色,包括应该透明的像素。

通过启用混合,我们使用以下方程式将最终颜色值设置为像素:

颜色[最终] = 颜色[源] * Alpha[源] + 颜色[目标] * 1- Alpha[源]

这里,源颜色和源 alpha 是文本在某个像素位置的颜色和 alpha 值,而目标颜色和 alpha 是颜色缓冲区中颜色和 alpha 的值。

在这个例子中,由于我们稍后绘制文本,目标颜色将是黄色,而源颜色,即文本,将是红色。目标 alpha 值为 1.0,而黄色是不透明的。对于文本,如果我们看一下 S 字母,例如,在 S 内部,即红色区域,它是完全不透明的,但它是透明的。

使用这个公式,让我们计算 S 周围透明区域的最终像素颜色,如下所示:

颜色[最终] = (1.0f, 0.0f, 0.0f, 0.0f) * 0.0 + (1.0f, 1.0f, 0.0f, 1.0f) * (1.0f- 0.0f)

= (1.0f, 1.0f, 0.0f, 1.0f);*

这只是黄色背景颜色。

相反,在 S 字母内部,它不是透明的,所以该像素位置的 alpha 值为 1。因此,当我们应用相同的公式时,我们得到最终颜色,如下所示:

颜色[最终] = (1.0f, 0.0f, 0.0f, 1.0f) * 1.0 + (1.0f, 1.0f, 0.0f, 1.0f) * (1.0f- 1.0f)

= (1.0f, 0.0f, 0.0f, 1.0f)

这只是红色文本颜色,如下面的图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/299dfb78-cf0f-4fac-97ca-908b9309fee7.png

让我们看看它在实践中是如何实现的。

  1. blend函数如下:
   glEnable(GL_BLEND); 

现在,我们需要设置源和目标混合因子,即 GL_SRC_ALPHA。对于源像素,我们使用其 alpha 值不变,而对于目标,我们将 alpha 设置为 GL_ONE_MINUS_SRC_ALPHA,即源 alpha 减去一,如下所示:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 

默认情况下,源值和目标值被添加。你也可以进行减法、加法和除法操作。

  1. 现在,我们需要调用 glUseProgram 函数来设置程序,将文本颜色设置为统一位置,并设置默认纹理,如下所示:
   glUseProgram(program); 
   glUniform3f(glGetUniformLocation(program, "textColor"), 
      this->color.x, this->color.y, this->color.z); 
   glActiveTexture(GL_TEXTURE0); 
  1. 接下来,我们需要绑定 VAO,如下所示:
   glBindVertexArray(VAO); 
  1. 让我们遍历我们要绘制的文本中的所有字符,获取它们的大小、偏移量,以便我们可以设置每个要绘制的字符的位置和纹理 ID,如下所示:
   std::string::const_iterator c; 

   for (c = text.begin(); c != text.end(); c++){ 

         Character ch = Characters[*c]; 

         GLfloat xpos = textPos.x + ch.Bearing.x * this->scale; 
         GLfloat ypos = textPos.y - (ch.Size.y - ch.Bearing.y) * 
         this->scale; 

         GLfloat w = ch.Size.x * this->scale; 
         GLfloat h = ch.Size.y * this->scale; 

         // Per Character Update VBO 
         GLfloat vertices[6][4] = { 
               { xpos, ypos + h, 0.0, 0.0 }, 
               { xpos, ypos, 0.0, 1.0 }, 
               { xpos + w, ypos, 1.0, 1.0 }, 

               { xpos, ypos + h, 0.0, 0.0 }, 
               { xpos + w, ypos, 1.0, 1.0 }, 
               { xpos + w, ypos + h, 1.0, 0.0 } 
         }; 

         // Render glyph texture over quad 
         glBindTexture(GL_TEXTURE_2D, ch.TextureID); 

         // Update content of VBO memory 
         glBindBuffer(GL_ARRAY_BUFFER, VBO); 

         // Use glBufferSubData and not glBufferData 
         glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), 
         vertices);  

         glBindBuffer(GL_ARRAY_BUFFER, 0); 

         // Render quad 
         glDrawArrays(GL_TRIANGLES, 0, 6); 

         // Now advance cursors for next glyph (note that advance 
         is number of 1/64 pixels) 
         // Bitshift by 6 to get value in pixels (2⁶ = 64 (divide 
         amount of 1/64th pixels by 64 to get amount of pixels)) 
         textPos.x += (ch.Advance >> 6) * this->scale;  
   } 

我们现在将绑定 VBO 并使用 glBufferSubData 将所有要绘制的四边形的顶点数据传递进去。一旦绑定,四边形将通过 glDrawArrays 绘制,我们传递 6 作为要绘制的顶点数。

然后,我们计算 textPos.x,这将决定下一个字符将被绘制的位置。我们通过将当前字符的进位乘以缩放并添加到当前文本位置的 x 分量来获取这个距离。对 advance 进行 6 比特的位移,以获取像素值。

  1. 在绘制函数的末尾,我们解绑顶点数组和纹理,然后禁用混合,如下所示:
glBindVertexArray(0); 
glBindTexture(GL_TEXTURE_2D, 0); 

glDisable(GL_BLEND);  
  1. 最后,我们添加 setPOsitonsetString 函数的实现,如下所示:
void TextRenderer::setPosition(glm::vec2 _position){ 

   this->position = _position; 
} 

void TextRenderer::setText(std::string _text){ 
   this->text = _text; 
} 

我们最终完成了 TextRenderer 类。现在,让我们学习如何在我们的游戏中显示文本:

  1. main.cpp 文件中,在文件顶部包含 TextRenderer.h 并创建一个名为 label 的类的新对象,如下所示:
#include "TextRenderer.h" 

TextRenderer* label;  

  1. 为文本着色程序创建一个新的 GLuint,如下所示:
GLuint textProgram 
  1. 然后,创建文本的新着色程序,如下所示:
textProgram = shader.CreateProgram("Assets/Shaders/text.vs", "Assets/Shaders/text.fs"); 
  1. text.vstext.fs 文件放置在 Assets 目录下的 Shaders.text.vs 中,如下所示:
#version 450 core 
layout (location = 0) in vec4 vertex; 
uniform mat4 projection; 

out vec2 TexCoords; 

void main(){ 
    gl_Position = projection * vec4(vertex.xy, 0.0, 1.0); 
    TexCoords = vertex.zw; 
}   

我们从属性中获取顶点位置和投影矩阵作为统一变量。纹理坐标在主函数中设置,并发送到下一个着色器阶段。四边形的顶点位置通过在 main() 函数中将局部坐标乘以正交投影矩阵来设置。

  1. 接下来,我们将进入片段着色器,如下所示:
#version 450 core 

in vec2 TexCoords; 

uniform sampler2D text; 
uniform vec3 textColor; 

out vec4 color; 

void main(){     
vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, TexCoords).r); 
color = vec4(textColor, 1.0) * sampled; 
} 

我们从顶点着色器获取纹理坐标和纹理以及颜色作为统一变量。创建一个新的 vec4 叫做颜色,用于发送颜色信息。在 main() 函数中,我们创建一个新的 vec4 叫做 sampled,并将 r、g 和 b 值存储为 1。我们还把红色颜色作为 alpha 值来绘制文本的不透明部分。然后,创建一个新的 vec4 叫做颜色,其中将白色颜色替换为我们想要文本绘制的颜色,并分配颜色变量。

  1. 让我们继续实现文本标签。在init函数中的addRigidBody函数之后,初始化label对象,如下所示:
label = new TextRenderer("Score: 0", "Assets/fonts/gooddog.ttf", 
        64, glm::vec3(1.0f, 0.0f, 0.0f), textProgram); 
   label->setPosition(glm::vec2(320.0f, 500.0f)); 

在构造函数中,我们设置要渲染的字符串,传入字体文件的路径,传入文本高度、文本颜色和文本程序。然后,我们使用setPosition函数设置文本的位置。

  1. 接下来,在tick函数中,我们更新分数时,也更新文本,如下所示:
         if (t.getOrigin().x() <= -18.0f) { 

               t.setOrigin(btVector3(18, 1, 0)); 
               score++; 
               label->setText("Score: " + std::to_string(score));
         }
  1. tick函数中,当游戏结束时,我们重置字符串,如下所示:
               gameover = true; 
               score = 0; 
               label->setText("Score: " + std::to_string(score));
  1. render函数中,我们调用draw函数来绘制文本,如下所示:
void renderScene(float dt){ 

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
   glClearColor(1.0, 1.0, 0.0, 1.0); 

   // Draw Objects 

   //light->draw(); 

   sphere->draw(); 
   enemy->draw(); 
   ground->draw(); 

   label->draw(); 
} 

由于 alpha 混合,文本必须在所有其他对象绘制完毕后绘制。

  1. 最后,确保字体文件已经添加到Fonts文件夹下的Assets文件夹中,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/3662e148-2020-4b4d-9b6f-31ff70aebe80.png

提供了一些字体文件,您可以进行实验。更多免费字体可以从www.1001freefonts.com/www.dafont.com/下载。构建并运行游戏以查看绘制的文本和更新:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/72630a6f-c8f6-4ab4-be2e-90c46938acc1.png

添加光照

最后,让我们给场景中的对象添加一些光照,以使对象看起来更有趣。我们将通过允许光照渲染器在场景中绘制来实现这一点。在这里,光照来自这个球体的中心。使用光源的位置,我们将计算像素是否被照亮,如下所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/b87c3061-eb22-4f78-b700-678bff35778c.png

左侧的图片显示了未照亮的场景。相比之下,右侧的场景使用了地球球体照明,地面受到光源的影响。面向光源的表面最亮,例如球体的顶部。这就在球体的顶部创建了一个镜面反射。由于表面离光源较远/与光源成角度,这些像素值逐渐扩散。然后,还有一些完全未面向光源的表面,例如面向我们的地面侧面。然而,它们仍然不是完全黑色,因为它们仍然受到来自光源的光照,这些光照反射并成为环境光的一部分。环境光漫反射镜面反射成为我们想要照亮物体时照明模型的主要部分。照明模型用于在计算机图形中模拟光照,因为我们受限于硬件的处理能力,这与现实世界不同。

根据 Phong 着色模型的像素最终颜色公式如下:

C = ka Lc+ Lc * max(0, n l) + ks * Lc * max(0, v r) p*

这里,我们有以下属性:

  • k[a ]是环境强度。

  • *L[c]*是光颜色。

  • n是表面法线。

  • l是光方向。

  • *k[s]*是镜面反射强度。

  • v是视图方向。

  • r 是关于表面法线的反射光方向。

  • p 是 Phong 指数,它将决定表面的光泽度。

对于 nlvr 向量,请参考以下图表:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/d0e6daa6-231d-4983-91eb-50bad4fbdbf3.png

让我们看看如何在实践中实现这一点:

  1. 所有的光照计算都是在对象的片段着色器中完成的,因为这将影响物体的最终颜色,这取决于光源和相机位置。对于每个要照明的物体,我们还需要传入光颜色、漫反射和镜面强度。在 MeshRenderer.h 文件中,更改构造函数,使其接受光源、漫反射和镜面强度,如下所示:
MeshRenderer(MeshType modelType, std::string _name, Camera * 
   _camera, btRigidBody* _rigidBody, LightRenderer* _light, float 
   _specularStrength, float _ambientStrength);
  1. 在文件顶部包含 lightRenderer.h,如下所示:
#include "LightRenderer.h"
  1. 在类的私有部分添加一个 LightRenderer 对象以及用于存储环境光和镜面强度的浮点数,如下所示:
        GLuint vao, vbo, ebo, texture, program; 
        LightRenderer* light;
        float ambientStrength, specularStrength;
  1. MeshRenderer.cpp 文件中,更改构造函数的实现,并将传入的变量分配给局部变量,如下所示:
MeshRenderer::MeshRenderer(MeshType modelType, std::string _name, 
   Camera* _camera, btRigidBody* _rigidBody, LightRenderer* _light, 
   float _specularStrength, float _ambientStrength) { 

   name = _name; 
   rigidBody = _rigidBody; 
   camera = _camera; 
   light = _light; 
   ambientStrength = _ambientStrength; 
   specularStrength = _specularStrength; 
... 
} 

  1. 在构造函数中,我们还需要添加一个新的法线属性,因为我们需要表面法线信息来进行光照计算,如下所示:
glEnableVertexAttribArray(0); 
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 
   (GLvoid*)0); 

 glEnableVertexAttribArray(1); 
 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), 
      (void*)(offsetof(Vertex, Vertex::texCoords))); 
 glEnableVertexAttribArray(2);
 glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 
     (void*)(offsetof(Vertex, Vertex::normal))); 

  1. Draw 函数中,我们将相机位置、光源位置、光源颜色、镜面强度和环境强度作为统一变量传递给着色器,如下所示:
   // Set Texture 
   glBindTexture(GL_TEXTURE_2D, texture); 

   // Set Lighting 
   GLuint cameraPosLoc = glGetUniformLocation(program, 
                         "cameraPos"); 
   glUniform3f(cameraPosLoc, camera->getCameraPosition().x,
   camera-> getCameraPosition().y, camera->getCameraPosition().z); 

   GLuint lightPosLoc = glGetUniformLocation(program, "lightPos"); 
   glUniform3f(lightPosLoc, this->light->getPosition().x, 
     this-> light->getPosition().y, this->light->getPosition().z); 

   GLuint lightColorLoc = glGetUniformLocation(program, 
                          "lightColor"); 
   glUniform3f(lightColorLoc, this->light->getColor().x, 
     this-> light->getColor().y, this->light->getColor().z); 

   GLuint specularStrengthLoc = glGetUniformLocation(program, 
                                "specularStrength"); 
   glUniform1f(specularStrengthLoc, specularStrength); 

   GLuint ambientStrengthLoc = glGetUniformLocation(
                               program, "ambientStrength"); 
   glUniform1f(ambientStrengthLoc, ambientStrength); 

   glBindVertexArray(vao);        
   glDrawElements(GL_TRIANGLES, indices.size(), 
      GL_UNSIGNED_INT, 0); 
   glBindVertexArray(0); 

  1. 我们还需要为效果创建新的顶点和片段着色器。让我们创建一个新的顶点着色器,称为 LitTexturedModel.vs,如下所示:
#version 450 core 
layout (location = 0) in vec3 position; 
layout (location = 1) in vec2 texCoord; 
layout (location = 2) in vec3 normal; 

out vec2 TexCoord; 
out vec3 Normal; 
out vec3 fragWorldPos; 

uniform mat4 vp; 
uniform mat4 model; 

void main(){ 

   gl_Position = vp * model *vec4(position, 1.0); 

   TexCoord = texCoord; 
   fragWorldPos = vec3(model * vec4(position, 1.0)); 
   Normal = mat3(transpose(inverse(model))) * normal; 

} 
  1. 我们添加新的位置布局以接收法线属性。

  2. 创建一个新的 out vec3,以便我们可以将法线信息发送到片段着色器。我们还将创建一个新的 out vec3 来发送片段的世界坐标。在 main() 函数中,我们通过将局部位置乘以世界矩阵来计算片段的世界位置,并将其存储在 fragWorldPos 变量中。法线也被转换为世界空间。与我们将局部位置相乘的方式不同,用于将法线转换为法线世界空间的模型矩阵需要以不同的方式处理。法线乘以模型矩阵的逆矩阵,并存储在法线变量中。这就是顶点着色器的内容。现在,让我们看看 LitTexturedModel.fs

  3. 在片段着色器中,我们获取纹理坐标、法线和片段世界位置。接下来,我们获取相机位置、光源位置和颜色、镜面和环境强度统一变量,以及作为统一变量的纹理。最终的像素值将存储在名为 colorout vec4 中,如下所示:

#version 450 core 

in vec2 TexCoord; 
in vec3 Normal; 
in vec3 fragWorldPos; 

uniform vec3 cameraPos; 
uniform vec3 lightPos; 
uniform vec3 lightColor; 

uniform float specularStrength; 
uniform float ambientStrength; 

// texture 
uniform sampler2D Texture; 

out vec4 color;    
  1. 在着色器的 main() 函数中,我们添加了光照计算,如下面的代码所示:
 void main(){ 

       vec3 norm = normalize(Normal); 
       vec4 objColor = texture(Texture, TexCoord); 

       //**ambient 
       vec3 ambient = ambientStrength * lightColor; 

       //**diffuse 
       vec3 lightDir = normalize(lightPos - fragWorldPos); 
       float diff = max(dot(norm, lightDir), 0.0); 
       vec3 diffuse = diff * lightColor; 

       //**specular  
       vec3 viewDir = normalize(cameraPos - fragWorldPos); 
       vec3 reflectionDir = reflect(-lightDir, norm); 
       float spec = pow(max(dot(viewDir, 
                    reflectionDir),0.0),128); 
       vec3 specular = specularStrength * spec * lightColor; 

       // lighting shading calculation 
       vec3 totalColor = (ambient + diffuse + specular) * 
       objColor.rgb; 

       color = vec4(totalColor, 1.0f); 

}  
  1. 我们首先获取法线和物体颜色。然后,根据公式方程,我们通过乘以环境强度和光颜色来计算方程的环境部分,并将其存储在名为ambientvec3中。对于方程的漫反射部分,我们通过从世界空间中像素的位置减去两个位置来计算光方向。结果向量被归一化并保存在vec3 lightDir中。然后,我们计算法线和光方向之间的点积。

  2. 之后,我们获取结果值或0中的较大值,并将其存储在名为diff的浮点数中。这个值乘以光颜色并存储在vec3中以获得漫反射颜色。对于方程的镜面反射部分,我们通过从相机位置减去片段世界位置来计算视图方向。

  3. 结果向量被归一化并存储在vec3 specDir中。然后,通过使用反射glsl内建函数并传入viewDir和表面法线来计算相对于表面法线的反射光向量。

  4. 然后,计算视图和反射向量的点积。选择计算值和0中的较大值。将得到的浮点值提高到128次幂。值可以从0256。值越大,物体看起来越亮。通过将镜面反射强度、计算的镜面反射值和存储在镜面vec3中的光颜色相乘来计算镜面反射值。

  5. 最后,通过将三个环境、漫反射和镜面反射值相加,然后乘以对象颜色来计算总的着色。对象颜色是一个vec4,所以我们将其转换为vec3。总颜色通过将totalColor转换为vec4分配给颜色变量。要在项目中实现这一点,创建一个新的着色程序,称为litTexturedShaderProgram

    按照以下方式:

GLuint litTexturedShaderProgram; 
Create the shader program and assign it to it in the init function in main.cpp. 
   litTexturedShaderProgram = shader.CreateProgram(
                              "Assets/Shaders/LitTexturedModel.vs",                 
                              "Assets/Shaders/LitTexturedModel.fs"); 
  1. 在添加rigidBody函数中,按照以下方式更改球体、地面和敌人的着色器:
  // Sphere Rigid Body 

  btCollisionShape* sphereShape = new btSphereShape(1);
  btDefaultMotionState* sphereMotionState = new 
     btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), 
     btVector3(0, 0.5, 0)));

  btScalar mass = 13.0f;
  btVector3 sphereInertia(0, 0, 0);
  sphereShape->calculateLocalInertia(mass, sphereInertia);

  btRigidBody::btRigidBodyConstructionInfo 
     sphereRigidBodyCI(mass, sphereMotionState, sphereShape, 
     sphereInertia);

  btRigidBody* sphereRigidBody = new btRigidBody
                                 (sphereRigidBodyCI);

  sphereRigidBody->setFriction(1.0f);
  sphereRigidBody->setRestitution(0.0f);

  sphereRigidBody->setActivationState(DISABLE_DEACTIVATION);

  dynamicsWorld->addRigidBody(sphereRigidBody);

  // Sphere Mesh

  sphere = new MeshRenderer(MeshType::kSphere, “hero”, 
           camera, sphereRigidBody, light, 0.1f, 0.5f);
  sphere->setProgram(litTexturedShaderProgram);
  sphere->setTexture(sphereTexture);
  sphere->setScale(glm::vec3(1.0f));

  sphereRigidBody->setUserPointer(sphere);

  // Ground Rigid body

  btCollisionShape* groundShape = new btBoxShape(btVector3(4.0f,   
                                  0.5f, 4.0f));
  btDefaultMotionState* groundMotionState = new 
    btDefaultMotionState(btTransform(btQuaternion(0, 0, 0, 1), 
     btVector3(0, -1.0f, 0)));

  btRigidBody::btRigidBodyConstructionInfo 
    groundRigidBodyCI(0.0f, groundMotionState, groundShape, 
    btVector3(0, 0, 0));

  btRigidBody* groundRigidBody = new btRigidBody
                                 (groundRigidBodyCI);

  groundRigidBody->setFriction(1.0);
  groundRigidBody->setRestitution(0.0);

  groundRigidBody->setCollisionFlags(
     btCollisionObject::CF_STATIC_OBJECT);

  dynamicsWorld->addRigidBody(groundRigidBody);

  // Ground Mesh
  ground = new MeshRenderer(MeshType::kCube, “ground”,
           camera, groundRigidBody, light, 0.1f, 0.5f);
  ground->setProgram(litTexturedShaderProgram);
  ground->setTexture(groundTexture);
  ground->setScale(glm::vec3(4.0f, 0.5f, 4.0f));

  groundRigidBody->setUserPointer(ground);

  // Enemy Rigid body

  btCollisionShape* shape = new btBoxShape(btVector3(1.0f, 
                            1.0f, 1.0f));
  btDefaultMotionState* motionState = new btDefaultMotionState(
      btTransform(btQuaternion(0, 0, 0, 1), 
      btVector3(18.0, 1.0f, 0)));
  btRigidBody::btRigidBodyConstructionInfo rbCI(0.0f, 
     motionState, shape, btVector3(0.0f, 0.0f, 0.0f));

  btRigidBody* rb = new btRigidBody(rbCI);

  rb->setFriction(1.0);
  rb->setRestitution(0.0);

  //rb->setCollisionFlags(btCollisionObject::CF_KINEMATIC_OBJECT);

  rb->setCollisionFlags(btCollisionObject::CF_NO_CONTACT_RESPONSE);

  dynamicsWorld->addRigidBody(rb);

  // Enemy Mesh
  enemy = new MeshRenderer(MeshType::kCube, “enemy”, 
          camera, rb, light, 0.1f, 0.5f);
  enemy->setProgram(litTexturedShaderProgram);
  enemy->setTexture(groundTexture);
  enemy->setScale(glm::vec3(1.0f, 1.0f, 1.0f));

  rb->setUserPointer(enemy);

  1. 构建并运行项目以查看光照着色器生效:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/b06faca5-471a-44ba-ac28-74bdc467bf34.png

作为练习,尝试向背景添加纹理,就像我们在 SFML 游戏中做的那样。

摘要

在本章中,我们看到了如何添加游戏对象之间的碰撞检测,然后通过添加控制和得分来完成了游戏循环。使用字体加载库 FreeType,我们将 TrueType 字体加载到我们的游戏中,以添加得分文本。最后,为了锦上添花,我们通过向对象添加 Phong 光照模型来添加场景中的光照。

在图形上,我们还可以添加很多内容来增加游戏的真实感,例如添加后处理效果的帧缓冲区。我们还可以添加如灰尘和雨的粒子效果。要了解更多信息,我强烈推荐learnopengl.com,如果你希望了解更多关于 OpenGL 的信息,它是一个惊人的资源。

在下一章中,我们将开始探索 Vulkan 渲染 API,并查看它与 OpenGL 的不同之处。

第四部分:使用 Vulkan 渲染 3D 对象

利用我们在上一节中获得的三维图形编程知识,我们现在可以在此基础上开发一个基本的 Vulkan 项目。OpenGL 是一个高级图形库。OpenGL 在后台做了很多用户通常不知道的事情。使用 Vulkan,我们将看到图形库的内部工作原理。我们将了解创建 SwapChains、图像视图、渲染过程、帧缓冲区、命令缓冲区的原因和方法,以及将对象渲染和呈现到场景中的过程。

以下章节包含在本节中:

第九章,Vulkan 入门

第十章,准备清除屏幕

第十一章,创建对象资源

第十二章,绘制 Vulkan 对象

第九章:Vulkan 入门

在前三个章节中,我们使用 OpenGL 进行渲染。虽然 OpenGL 适合开发原型并快速开始渲染,但它确实有其弱点。首先,OpenGL 非常依赖驱动程序,这使得它在性能方面较慢且不可预测,这也是为什么我们更喜欢使用 Vulkan 进行渲染的原因。

在本章中,我们将涵盖以下主题:

  • 关于 Vulkan

  • 配置 Visual Studio

  • Vulkan 验证层和扩展

  • Vulkan 实例

  • Vulkan 上下文类

  • 创建窗口表面

  • 选择物理设备并创建逻辑设备

关于 Vulkan

使用 OpenGL 时,开发者必须依赖 NVIDIA、AMD 和 Intel 等厂商发布适当的驱动程序,以便在游戏发布前提高游戏性能。只有当开发者与厂商紧密合作时,这才能实现。如果不是这样,厂商只能在游戏发布后才能发布优化驱动程序,并且发布新驱动程序可能需要几天时间。

此外,如果你想要将你的 PC 游戏移植到移动平台,并且你使用 OpenGL 作为渲染器,你将需要将渲染器移植到 OpenGLES,它是 OpenGL 的一个子集,其中 ES 代表嵌入式系统。尽管 OpenGL 和 OpenGLES 之间有很多相似之处,但要使其在其他平台上工作,仍然需要做额外的工作。为了减轻这些问题,引入了 Vulkan。Vulkan 通过减少驱动程序的影响并提供明确的开发者控制来提高游戏性能,从而赋予开发者更多的控制权。

Vulkan 是从底层开发的,因此与 OpenGL 不向后兼容。当使用 Vulkan 时,你可以完全访问 GPU。

使用完整的 GPU 访问,你也有完全的责任来实现渲染 API。因此,使用 Vulkan 的缺点在于,当你用它进行开发时,你必须指定一切。

总的来说,这使得 Vulkan 成为一个非常冗长的 API,你必须指定一切。然而,这也使得当 GPU 添加新功能时,很容易为 Vulkan 的 API 规范创建扩展。

配置 Visual Studio

Vulkan 只是一个渲染 API,因此我们需要创建一个窗口并进行数学运算。对于这两者,我们将使用 GLFW 和 GLM,就像我们创建 OpenGL 项目时一样。为此,请按照以下步骤操作:

  1. 创建一个新的 Visual Studio C++项目,并将其命名为VulkanProject

  2. 将 OpenGL 项目中的GLFWGLM文件夹复制到VulkanProject文件夹中,放在名为Dependencies的文件夹内。

  3. 下载 Vulkan SDK。访问 vulkan.lunarg.com/sdk/home 并下载 SDK 的 Windows 版本,如以下截图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/b4e84ef1-92b7-4289-99da-003f9c0499aa.png

  1. 按照以下截图所示安装 SDK:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/e0b1f20e-59ef-462b-9a8f-c16a4e217576.png

  1. Dependencies 目录中创建一个名为 Vulkan 的新文件夹。从 Vulkan SDK 文件夹中复制并粘贴 Lib 和包含文件夹到 C:\ 驱动器,如图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/734566c4-d064-467a-abfd-0afc5c4c1435.png

  1. 在 Visual Studio 项目中,创建一个新的空白 source.cpp 文件。打开 Vulkan 项目属性,并将 include 目录添加到 C/C+ | 通用 | 额外包含目录。

  2. 确保在配置和平台下拉列表中选择了所有配置和所有平台,如图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/405981b3-65ce-41a2-9cf2-d8a11977c6f4.png

  1. 在链接器 | 通用部分下添加库目录,如图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/4a23c330-5cbd-4ec9-bb32-379484c85e9b.png

  1. 在链接器 | 输入中设置您想要使用的库,如图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/3de238cf-8de9-4f3b-a2d6-b4b1d761aff5.png

在完成准备工作后,让我们检查我们的窗口创建是否正常工作:

  1. 在 source.cpp 中添加以下代码:

#defineGLFW_INCLUDE_VULKAN 
#include<GLFW/glfw3.h> 

int main() { 

   glfwInit(); 

   glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 
   glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); 

   GLFWwindow* window = glfwCreateWindow(1280, 720, "HELLO VULKAN ", nullptr, nullptr); 

   while (!glfwWindowShouldClose(window)) { 

         glfwPollEvents(); 
   } 

   glfwDestroyWindow(window); 
   glfwTerminate(); 

   return 0; 
}

首先,我们包含 glfw3.h 并让 GLFW 包含一些与 Vulkan 相关的头文件。然后,在主函数中,我们通过调用 glfwInit() 初始化 GLFW。然后,我们调用 glfwWindowHint 函数。第一个 glfwWindowHint 函数不会创建 OpenGL 上下文,因为它默认由 Glfw 创建。在下一个函数中,我们禁用了即将创建的窗口的调整大小功能。

然后,我们以创建 OpenGL 项目中创建窗口的类似方式创建一个 1,280 x 720 的窗口。我们创建一个 while 循环来检查窗口是否应该关闭。如果窗口不需要关闭,我们将轮询系统事件。一旦完成,我们将销毁窗口,终止 glfw,并返回 0

  1. 这应该会给我们一个可以工作的窗口。以调试模式作为 x64 可执行文件运行应用程序,以查看显示的窗口和显示的 HELLO VULKAN,如图所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/f8b4a3d5-0065-4d80-91a3-990828b2e507.png

Vulkan 验证层和扩展

在我们开始创建 Vulkan 应用程序之前,我们必须检查应用程序验证层和扩展。让我们更详细地了解一下:

  • 验证层:由于给予开发者的控制权很大,开发者也有可能以错误的方式实现 Vulkan 应用程序。Vulkan 验证层会检查这些错误,并告知开发者他们正在做错事,需要修复。

  • 扩展:在 Vulkan API 的开发过程中,可能会为新的 GPU 引入新功能。为了保持 Vulkan 的更新,我们需要通过添加扩展来扩展其功能。

这类的一个例子是在 RTX 系列 GPU 中引入光线追踪。在 Vulkan 中,创建了一个新的扩展来支持 NVIDIA 在硬件上的这一变化,即Vk_NV_ray_tracing。如果我们的游戏使用这个扩展,我们可以检查硬件是否支持它。

类似的扩展也可以在应用程序级别添加和检查。其中一个这样的扩展是调试报告扩展,当我们在实现 Vulkan 时出现问题时,我们可以生成这个扩展。我们的第一个类将向应用程序添加此功能,以检查应用程序验证层和扩展。

让我们开始创建我们的第一个类。创建一个名为AppValidationLayersAndExtensions的新类。在AppValidationLayersAndExtensions.h中,添加以下代码:

#pragmaonce 

#include<vulkan\vulkan.h> 
#include<vector> 
#include<iostream> 

#defineGLFW_INCLUDE_VULKAN 
#include<GLFW\glfw3.h> 

classAppValidationLayersAndExtensions { 

public: 
   AppValidationLayersAndExtensions(); 
   ~AppValidationLayersAndExtensions(); 

   const std::vector<constchar*> requiredValidationLayers = { 
         "VK_LAYER_LUNARG_standard_validation" 
   }; 

   bool checkValidationLayerSupport(); 
   std::vector<constchar*>getRequiredExtensions
     (boolisValidationLayersEnabled); 

   // Debug Callback 
   VkDebugReportCallbackEXT callback; 

   void setupDebugCallback(boolisValidationLayersEnabled, 
      VkInstancevkInstance); 
   void destroy(VkInstanceinstance, boolisValidationLayersEnabled); 

   // Callback 

* pCreateInfo, VkResultcreateDebugReportCallbackEXT( 
       VkInstanceinstance, 
       constVkDebugReportCallbackCreateInfoEXT         
       constVkAllocationCallbacks* pAllocator, 
       VkDebugReportCallbackEXT* pCallback) { 

         auto func = (PFN_vkCreateDebugReportCallbackEXT)
                     vkGetInstanceProcAddr(instance, 
                     "vkCreateDebugReportCallbackEXT"); 

         if (func != nullptr) { 
               return func(instance, pCreateInfo, pAllocator, pCallback); 
         } 
         else { 
               returnVK_ERROR_EXTENSION_NOT_PRESENT; 
         } 

   } 

   void DestroyDebugReportCallbackEXT( 
         VkInstanceinstance, 
         VkDebugReportCallbackEXTcallback, 
         constVkAllocationCallbacks* pAllocator) { 

         auto func = (PFN_vkDestroyDebugReportCallbackEXT)
                     vkGetInstanceProcAddr(instance, 
                     "vkDestroyDebugReportCallbackEXT"); 
         if (func != nullptr) { 
               func(instance, callback, pAllocator); 
         } 
   } 

}; 

我们包含vulkan.hiostreamvectorglfw。然后,我们创建一个名为requiredValidationLayers的向量;这是我们将VK_LAYER_LUNARG_standard_validation传递的地方。对于我们的应用程序,我们需要标准验证层,其中包含所有验证层。如果我们只需要特定的验证层,我们也可以单独指定它们。接下来,我们创建两个函数:一个用于检查验证层的支持,另一个用于获取所需的扩展。

为了在发生错误时生成报告,我们需要一个调试回调。我们将向其中添加两个函数:一个用于设置调试回调,另一个用于销毁它。这些函数将调用debugcreatedestroy函数;它们将调用vkGetInstanceProcAddr以获取vkCreateDebugReportCallbackEXTvkDestroyDebugReportCallbackEXT指针函数的指针,以便我们可以调用它们。

如果生成调试报告不那么令人困惑会更好,但不幸的是,这就是必须这样做的方式。然而,我们只需要做一次。让我们继续实施AppValidationLayersAndExtentions.cpp

  1. 首先,我们添加构造函数和析构函数,如下所示:
AppValidationLayersAndExtensions::AppValidationLayersAndExtensions(){} 

AppValidationLayersAndExtensions::~AppValidationLayersAndExtensions(){} 
Then we add the implementation to checkValidationLayerSupport(). 

bool AppValidationLayersAndExtensions::checkValidationLayerSupport() { 

   uint32_t layerCount; 

   // Get count of validation layers available 
   vkEnumerateInstanceLayerProperties(&layerCount, nullptr); 

   // Get the available validation layers names  
   std::vector<VkLayerProperties>availableLayers(layerCount); 
   vkEnumerateInstanceLayerProperties(&layerCount,
   availableLayers.data()); 

   for (const char* layerName : requiredValidationLayers) { //layers we
   require 

         // boolean to check if the layer was found 
         bool layerFound = false; 

         for (const auto& layerproperties : availableLayers) { 

               // If layer is found set the layar found boolean to true 
               if (strcmp(layerName, layerproperties.layerName) == 0) { 
                     layerFound = true; 
                     break; 
               } 
         } 

         if (!layerFound) { 
               return false; 
         } 

         return true; 

   } 

}

要检查支持的验证层,调用vkEnumerateInstanceLayerProperties函数两次。我们第一次调用它以获取可用的验证层数量。一旦我们有了计数,我们再次调用它以填充层的名称。

我们创建一个名为layerCountint,并在第一次调用vkEnumerateInstanceLayerProperties时传入它。该函数接受两个参数:第一个是计数,第二个最初保持为null。一旦函数被调用,我们将知道有多少验证层可用。对于层的名称,我们创建一个新的名为availableLayersVkLayerProperties类型向量,并用layerCount初始化它。然后,函数再次被调用,这次我们传入layerCount和向量作为参数来存储信息。之后,我们在所需层和可用层之间进行检查。如果找到了验证层,函数将返回true。如果没有找到,它将返回false

  1. 接下来,我们需要添加getRequiredInstanceExtensions函数,如下所示:
std::vector<constchar*>AppValidationLayersAndExtensions::getRequiredExtensions(boolisValidationLayersEnabled) { 

   uint32_t glfwExtensionCount = 0; 
   constchar** glfwExtensions; 

   // Get extensions 
   glfwExtensions = glfwGetRequiredInstanceExtensions
                    (&glfwExtensionCount); 

   std::vector<constchar*>extensions(glfwExtensions, glfwExtensions 
     + glfwExtensionCount); 

   //debug report extention is added. 

   if (isValidationLayersEnabled) { 
         extensions.push_back("VK_EXT_debug_report"); 
   } 

   return extensions; 
}

getRequiredInstanceExtensions短语将获取由GLFW支持的扩展。它接受一个布尔值来检查验证层是否启用,并返回一个包含支持扩展名称的向量。在这个函数中,我们创建一个名为glfwExtensionCountunint32_t和一个用于存储扩展名称的const char。我们调用glfwGetRequiredExtensions,传入glfwExtensionCount,并将其设置为等于glfwExtensions。这将把所有必需的扩展存储在glfwExtensions中。

我们创建一个新的扩展向量,并存储glfwExtention名称。如果我们启用了验证层,则可以添加一个额外的扩展层,称为VK_EXT_debug_report,这是用于生成调试报告的扩展。这个扩展向量在函数结束时返回。

  1. 然后,我们添加调试报告回调函数,该函数将在出现错误时生成报告消息,如下所示:
 staticVKAPI_ATTRVkBool32VKAPI_CALL debugCallback( 
   VkDebugReportFlagsEXTflags, 
   VkDebugReportObjectTypeEXTobjExt, 
   uint64_tobj, 
   size_tlocation, 
   int32_tcode, 
   constchar* layerPrefix, 
   constchar* msg, 
   void* userData) { 

   std::cerr <<"validation layer: "<<msg<< std::endl << std::endl; 

   returnfalse; 

} 
  1. 接下来,我们需要创建setupDebugCallback函数,该函数将调用createDebugReportCallbackExt函数,如下所示:
voidAppValidationLayersAndExtensions::setupDebugCallback(boolisValidationLayersEnabled, VkInstancevkInstance) { 

   if (!isValidationLayersEnabled) { 
         return; 
   } 

   printf("setup call back \n"); 

   VkDebugReportCallbackCreateInfoEXT info = {}; 

   info.sType = VK_STRUCTURE_TYPE_DEBUG_REPORT
                _CALLBACK_CREATE_INFO_EXT; 
   info.flags = VK_DEBUG_REPORT_ERROR_BIT_EXT | 
                VK_DEBUG_REPORT_WARNING_BIT_EXT; 
   info.pfnCallback = debugCallback; // callback function 

   if (createDebugReportCallbackEXT(vkInstance, &info, nullptr, 
     &callback) != VK_SUCCESS) { 

         throw std::runtime_error("failed to set debug callback!"); 
   } 

} 

此函数接受一个布尔值,用于检查验证层是否启用。它还接受一个 Vulkan 实例,我们将在本类之后创建它。

在创建 Vulkan 对象时,我们通常必须用所需的参数填充一个结构体。因此,要创建DebugReportCallback,我们首先必须填充VkDebugReportCallbackCreateInfoExt结构体。在这个结构体中,我们传入sType,它指定了结构体类型。我们还传入任何用于错误和警告报告的标志。最后,我们传入callback函数本身。然后,我们调用createDebugReportCallbackExt函数,传入实例、结构体、用于内存分配的空指针和callback函数。尽管我们为内存分配传入了一个空指针,但 Vulkan 将自行处理内存分配。如果你有自己的内存分配函数,此函数是可用的。

  1. 现在,让我们创建destroy函数,以便我们可以销毁调试报告callback函数,如下所示:
voidAppValidationLayersAndExtensions::destroy(VkInstanceinstance, boolisValidationLayersEnabled){ 

   if (isValidationLayersEnabled) { 
         DestroyDebugReportCallbackEXT(instance, callback, nullptr); 
   } 

} 

Vulkan 实例

要使用 AppValidationLayerAndExtension 类,我们必须创建一个 Vulkan 实例。为此,请按照以下步骤操作:

  1. 我们将创建另一个名为 VulkanInstance 的类。在 VulkanInstance.h 中,添加以下代码:
#pragmaonce 
#include<vulkan\vulkan.h> 

#include"AppValidationLayersAndExtensions.h" 

classVulkanInstance 
{ 
public: 
   VulkanInstance(); 
   ~VulkanInstance(); 

   VkInstance vkInstance; 

   void createAppAndVkInstance(,boolenableValidationLayers  
        AppValidationLayersAndExtensions *valLayersAndExtentions); 

};  

我们包含 vulkan.hAppValidationLayersAndExtentions.h,因为我们创建 Vulkan 实例时将需要所需的验证层和扩展。我们添加了构造函数、析构函数以及 VkInstance 的实例,以及一个名为 ceeateAppAndVkInstance 的函数。这个函数接受一个布尔值,用于检查验证层是否启用,以及 AppValidationLayersAndExtensions。这就是头文件的内容。

  1. .cpp 文件中,添加以下代码:
#include"VulkanInstance.h" 

VulkanInstance::VulkanInstance(){} 

VulkanInstance::~VulkanInstance(){}
  1. 然后添加 createAppAndVkInstance 函数,这将允许我们创建 Vulkan 实例,如下所示:
voidVulkanInstance::createAppAndVkInstance(boolenableValidationLayers, AppValidationLayersAndExtensions *valLayersAndExtentions) { 

   // links the application to the Vulkan library 

   VkApplicationInfo appInfo = {}; 
   appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; 
   appInfo.pApplicationName = "Hello Vulkan"; 
   appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); 
   appInfo.pEngineName = "SidTechEngine"; 
   appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); 
   appInfo.apiVersion = VK_API_VERSION_1_0; 

   VkInstanceCreateInfo vkInstanceInfo = {}; 
   vkInstanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; 
   vkInstanceInfo.pApplicationInfo = &appInfo; 

   // specify extensions and validation layers 
   // these are global meaning they are applicable to whole program 
      not just the device 

   auto extensions = valLayersAndExtentions->
                   getRequiredExtensions(enableValidationLayers); 

   vkInstanceInfo.enabledExtensionCount = static_cast<uint32_t>
      (extensions.size());; 
   vkInstanceInfo.ppEnabledExtensionNames = extensions.data(); 

   if (enableValidationLayers) { 
     vkInstanceInfo.enabledLayerCount = static_cast<uint32_t>
     (valLayersAndExtentions->requiredValidationLayers.size()); 
     vkInstanceInfo.ppEnabledLayerNames = 
     valLayersAndExtentions->requiredValidationLayers.data(); 
   } 
   else { 
         vkInstanceInfo.enabledLayerCount = 0; 
   } 
  if (vkCreateInstance(&vkInstanceInfo, nullptr, &vkInstance) !=
   VK_SUCCESS) {
   throw std::runtime_error("failed to create vkInstance ");
  }
}   

在前面的函数中,我们必须填充 VkApplicationInfostruct,这在创建 VkInstance 时是必需的。然后,我们创建 appInfo 结构体。在这里,我们指定的第一个参数是 struct 类型,它是 VK_STRUCTURE_TYPE_APPLICATION_INFO 类型。下一个参数是应用程序名称本身,我们在这里指定应用程序版本,版本号为 1.0。然后,我们指定引擎名称和版本。最后,我们指定要使用的 Vulkan API 版本。

一旦应用程序 struct 已被填充,我们可以创建 vkInstanceCreateInfo 结构体,这将创建 Vulkan 实例。在我们创建的结构体实例中——就像之前的所有结构体一样——我们必须指定具有 struct 类型的结构体,它是 VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO

然后,我们必须传递应用程序信息结构体。我们必须指定 Vulkan 扩展和验证层以及计数。这些信息是从 AppValidationLayersAndExtensions 类中检索的。验证层仅在类处于调试模式时启用;否则,它不会被启用。

现在,我们可以通过调用 vkCreateInstance 函数来创建 Vulkan 实例。这个函数有三个参数:创建信息实例、分配器和用于存储 Vulkan 实例的实例变量。对于分配,我们指定 nullptr 并让 Vulkan 处理内存分配。如果 Vulkan 实例没有创建,将在控制台打印运行时错误,表示函数未能创建 Vulkan 实例。

为了使用这个 ValidationAndExtensions 类和 Vulkan 实例类,我们将创建一个新的 Singleton 类,名为 VulkanContext。我们这样做是因为在创建我们的 ObjectRenderer 时,我们需要访问这个类中的一些 Vulkan 对象。

Vulkan 上下文类

Vulkan 上下文类将包含我们创建 Vulkan 渲染器所需的所有功能。在这个类中,我们将创建验证层,创建 Vulkan 应用程序和实例,选择我们想要使用的 GPU,创建 swapchain,创建渲染目标,创建渲染通道,并添加命令缓冲区,以便我们可以将我们的绘图命令发送到 GPU。

我们还将添加两个新的函数:drawBegindrawEnd。在drawBegin函数中,我们将添加绘图准备阶段的函数。drawEnd函数将在我们绘制一个对象并准备它以便可以在视口中呈现之后被调用。

创建一个新的.h类文件和.cpp文件。在.h文件中,包含以下代码:

#defineGLFW_INCLUDE_VULKAN 
#include<GLFW\glfw3.h> 

#include<vulkan\vulkan.h> 

#include"AppValidationLayersAndExtensions.h" 
#include"VulkanInstance.h" 

接下来,我们将创建一个布尔值isValidationLayersEnabled。如果应用程序以调试模式运行,则将其设置为true;如果以发布模式运行,则设置为false

#ifdef _DEBUG 
boolconstbool isValidationLayersEnabled = true; 
#else 
constbool isValidationLayersEnabled = false; 
#endif 

接下来,我们创建类本身,如下所示:

classVulkanContext { 

public: 

staticVulkanContextn* instance;   
staticVulkanContext* getInstance(); 

   ~VulkanContext(); 

   void initVulkan(); 

private: 

   // My Classes 
   AppValidationLayersAndExtensions *valLayersAndExt; 
   VulkanInstance* vInstance; 

};

public部分,我们创建一个静态实例和getInstance变量和函数,该函数用于设置和获取这个类的实例。我们添加了析构函数并添加了一个initVulkan函数,该函数将用于初始化 Vulkan 上下文。在private部分,我们创建了AppValidationLayersAndExtensionsVulkanInstance类的实例。在VulkanContext.cpp文件中,我们将实例变量设置为null,并在getInstance函数中检查实例是否已创建。如果没有创建,我们创建一个新的实例,返回它,并添加析构函数:

#include"VulkanContext.h" 

VulkanContext* VulkanContext::instance = NULL; 

VulkanContext* VulkanContext::getInstance() { 

   if (!instance) { 
         instance = newVulkanContext(); 
   } 
   return instance; 
} 

VulkanContext::~VulkanContext(){ 

然后,我们添加initVulkan函数的功能,如下所示:

voidVulkanContext::initVulkan() { 

   // Validation and Extension Layers 
   valLayersAndExt = newAppValidationLayersAndExtensions(); 

   if (isValidationLayersEnabled && !valLayersAndExt->
     checkValidationLayerSupport()) { 
         throw std::runtime_error("Validation Layers 
           Not Available !"); 
   } 

   // Create App And Vulkan Instance() 
   vInstance = newVulkanInstance(); 
   vInstance->createAppAndVkInstance(isValidationLayersEnabled, 
      valLayersAndExt); 

   // Debug CallBack 
   valLayersAndExt->setupDebugCallback(isValidationLayersEnabled, 
     vInstance->vkInstance); 

}  

首先,我们创建一个新的AppValidationLayersAndExtensions实例。然后,我们检查验证层是否启用并检查验证层是否受支持。如果ValidationLayers不可用,将发出运行时错误,表示验证层不可用。

如果验证层受支持,将创建一个新的VulkanInstance类实例并调用createAppAndVkInstance函数,该函数创建一个新的vkInstance

完成这些后,我们通过传递布尔值和vkInstance调用setupDebugCallBack函数。在source.cpp文件中,包含VulkanContext.h文件,并在窗口创建后调用initVulkan,如下所示:

 #defineGLFW_INCLUDE_VULKAN 
#include<GLFW/glfw3.h> 

#include"VulkanContext.h" 

int main() { 

   glfwInit(); 

   glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); 
   glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); 

   GLFWwindow* window = glfwCreateWindow(1280, 720, "HELLO VULKAN ", 
                        nullptr, nullptr); 

   VulkanContext::getInstance()->initVulkan(); 

   while (!glfwWindowShouldClose(window)) { 

         glfwPollEvents(); 
   }               

   glfwDestroyWindow(window); 
   glfwTerminate(); 

   return 0; 
}

希望你在构建和运行应用程序时不会在控制台窗口中遇到任何错误。如果你遇到错误,请逐行检查代码,确保没有拼写错误:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/5a01dea9-20d8-4215-824d-c92cbb68c200.png

创建窗口表面

我们需要一个针对当前平台创建的窗口的接口,以便我们可以展示我们将要渲染的图像。我们使用VKSurfaceKHR属性来获取对窗口表面的访问权限。为了存储操作系统支持的表面信息,我们将调用glfw函数glfwCreateWindowSurface来创建操作系统支持的表面。

VulkanContext.h中,添加一个名为surface的新变量,类型为VkSurfaceKHR,如下所示:

private: 

   //surface 
   VkSurfaceKHR surface; 

由于我们需要访问在source.cpp中创建的窗口实例,因此更改initVulkan函数,使其接受一个GLFWwindow,如下所示:

   void initVulkan(GLFWwindow* window); 

VulkanContext.cpp中,更改initVulkan的实现,如下所示,并调用glfwCreateWindowSurface函数,该函数接受 Vulkan 实例和窗口。接下来,传入null作为分配器和表面以创建表面对象:

 void VulkanContext::initVulkan(GLFWwindow* window) { 

   // -- Platform Specific 

   // Validation and Extension Layers 
   valLayersAndExt = new AppValidationLayersAndExtensions(); 

   if (isValidationLayersEnabled && !valLayersAndExt->
      checkValidationLayerSupport()) { 
         throw std::runtime_error("Requested Validation Layers
            Not Available !"); 
   } 

   // Create App And Vulkan Instance() 
   vInstance = new VulkanInstance(); 
   vInstance->createAppAndVkInstance(isValidationLayersEnabled, 
     valLayersAndExt); 

   // Debug CallBack 
   valLayersAndExt->setupDebugCallback(isValidationLayersEnabled, 
    vInstance->vkInstance); 

   // Create Surface 
   if (glfwCreateWindowSurface(vInstance->vkInstance, window, 
      nullptr, &surface) != VK_SUCCESS) { 

         throw std::runtime_error(" failed to create window 
           surface !"); 
   } 
} 

最后,在source.cpp中更改initVulkan函数,如下所示:

   GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, 
                        "HELLO VULKAN ", nullptr, nullptr); 

   VulkanContext::getInstance()->initVulkan(window); 

选择物理设备并创建逻辑设备

现在,我们将创建Device类,它将用于遍历我们拥有的不同物理设备。我们将选择一个来渲染我们的应用程序。为了检查您的 GPU 是否与 Vulkan 兼容,请检查 GPU 供应商网站上的兼容性列表,或访问en.wikipedia.org/wiki/Vulkan_(API)

基本上,任何来自 Geforce 600 系列以及 Radeon HD 2000 系列及以后的 NVIDIA GPU 都应该得到支持。为了访问物理设备并创建逻辑设备,我们将创建一个新的类,这样我们就可以随时访问它。创建一个名为Device的新类。在Device.h中添加以下包含:

#include<vulkan\vulkan.h> 
#include<stdexcept> 

#include<iostream> 
#include<vector> 
#include<set> 

#include"VulkanInstance.h" 
#include"AppValidationLayersAndExtensions.h" 

为了方便起见,我们还将添加几个结构体。第一个叫做SwapChainSupportDetails;它能够访问VkSurfaceCapabilitiesKHR,其中包含有关表面的所有所需详细信息。我们还将添加VkSurfaceFormatKHR类型的surfaceFormats向量,它跟踪表面支持的所有不同图像格式,以及VkPresentModeKHR类型的presentModes向量,它存储 GPU 支持的显示模式。

渲染的图像将被发送到窗口表面并显示。这就是我们能够使用渲染器(如 OpenGL 或 Vulkan)看到最终渲染图像的原因。现在,我们可以一次显示这些图像,如果我们想永远查看静态图像,这是可以的。然而,当我们运行每 16 毫秒更新一次(每秒 60 次)的游戏时,可能会出现图像尚未完全渲染,但需要显示的情况。在这种情况下,我们会看到半渲染的图像,这会导致屏幕撕裂。

为了避免这种情况,我们使用双缓冲。这允许我们渲染图像,使其具有两个不同的图像,称为前缓冲区和后缓冲区,并在它们之间进行 ping-pong。然后,我们展示已经完成渲染的缓冲区,并在下一个帧仍在渲染时将其显示到视口中,如下面的图所示。还有不同的方式来展示图像。当我们创建 swapchain 时,我们将查看这些不同的呈现模式:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/ee9c08d1-1be2-4404-8aca-76b1e698d73a.png

我们需要创建一个结构体来跟踪表面属性、格式和呈现模式,如下所示:

structSwapChainSupportDetails { 

   VkSurfaceCapabilitiesKHR surfaceCapabilities; // size and images 
                                                  in swapchain 
   std::vector<VkSurfaceFormatKHR> surfaceFormats; 
   std::vector<VkPresentModeKHR> presentModes; 
}; 

GPU 也有被称为QueueFamilies的东西。命令被发送到 GPU,然后使用队列执行。有针对不同类型工作的单独队列。渲染命令被发送到渲染队列,计算命令被发送到计算队列,还有用于展示图像的呈现队列。我们还需要知道 GPU 支持哪些队列以及有多少队列存在。

渲染器、计算和呈现队列可以组合,并被称为队列家族。这些队列可以以不同的方式组合,形成多个队列家族。这意味着可以组合渲染和呈现队列形成一个队列家族,而另一个家族可能只包含计算队列。因此,我们必须检查我们是否至少有一个包含图形和呈现队列的队列家族。这是因为我们需要一个图形队列来传递我们的渲染命令,以及一个呈现队列在渲染后展示图像。

我们将添加一个结构体来检查这两个方面,如下所示:

structQueueFamilyIndices { 

   int graphicsFamily = -1; 
   int presentFamily = -1; 

   bool arePresent() { 
         return graphicsFamily >= 0 && presentFamily >= 0; 
   } 
}; 

现在,我们将创建Device类本身。在创建类之后,我们添加构造函数和析构函数,如下所示:

 { 

public: 

   Device(); 
   ~Device();  

然后,我们需要添加一些变量,以便我们可以存储物理设备、SwapChainSupportDetailsQueueFamilyIndices,如下所示:

   VkPhysicalDevice physicalDevice; 
   SwapChainSupportDetails swapchainSupport; 
   QueueFamilyIndices queueFamiliyIndices; 

要创建双缓冲,我们必须检查设备是否支持它。这是通过使用VK_KHR_SWAPCHAIN_EXTENSION_NAME扩展来完成的,该扩展检查 swapchain。首先,我们创建一个char*常量向量,并传入扩展名称,如下所示:

std::vector<constchar*>deviceExtensions = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

然后,我们添加了pickPhysicalDevice函数,该函数将根据设备是否合适来选择。在检查合适性的过程中,我们将检查所选设备是否支持 swapchain 扩展,获取 swapchain 支持详情,以及获取队列家族索引,如下所示:

   void pickPhysicalDevice (VulkanInstance* vInstance, 
     VkSurfaceKHR surface); 

   bool isDeviceSuitable(VkPhysicalDevice device, 
     VkSurfaceKHR surface); 

   bool checkDeviceExtensionSupported(VkPhysicalDevice device) ; 
   SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice 
      device, VkSurfaceKHR surface); 
   QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device, 
      VkSurfaceKHR surface); 

我们还将添加一个获取器函数来获取当前设备的队列家族,如下所示:

 QueueFamilyIndicesgetQueueFamiliesIndicesOfCurrentDevice();  

一旦我们有了想要使用的物理设备,我们将创建一个逻辑设备的实例。逻辑设备是物理设备本身的接口。我们将使用逻辑设备来创建缓冲区等。我们还将存储当前设备的图形和呈现队列,以便我们可以发送图形和呈现命令。最后,我们将添加一个destroy函数,用于销毁我们创建的物理和逻辑设备,如下所示:

   // ++++++++++++++ 
   // Logical device 
   // ++++++++++++++ 

   void createLogicalDevice(VkSurfaceKHRsurface, 
      boolisValidationLayersEnabled, AppValidationLayersAndExtensions 
      *appValLayersAndExtentions); 

   VkDevice logicalDevice; 

   // handle to the graphics queue from the queue families of the gpu 
   VkQueue graphicsQueue; // we can also have seperate queue for 
                            compute, memory transfer, etc. 
   VkQueue presentQueue; // queue for displaying the framebuffer 

   void destroy(); 
}; // End of Device class

Device.h文件的内容就到这里。让我们继续到Device.cpp。首先,我们包含Device.h并添加构造函数和析构函数,如下所示:

#include"Device.h" 

Device::Device(){} 

Device::~Device(){ 

} 

现在,真正的任务开始了。我们需要创建一个pickPhysicalDevice函数,它接受一个 Vulkan 实例和VkSurface,如下所示:


voidDevice::pickPhysicalDevice(VulkanInstance* vInstance, VkSurfaceKHRsurface) { 

   uint32_t deviceCount = 0; 

   vkEnumeratePhysicalDevices(vInstance->vkInstance, &deviceCount, 
      nullptr); 

   if (deviceCount == 0) { 
         throw std::runtime_error("failed to find GPUs with vulkan 
           support !"); 
   } 

   std::cout <<"Device Count: "<< deviceCount << std::endl; 

   std::vector<VkPhysicalDevice>devices(deviceCount); 
   vkEnumeratePhysicalDevices(vInstance->vkInstance, &deviceCount, 
      devices.data()); 

   std::cout << std::endl; 
   std::cout <<"DEVICE PROPERTIES"<< std::endl; 
   std::cout <<"================="<< std::endl; 

   for (constauto& device : devices) { 

         VkPhysicalDeviceProperties  deviceProperties; 

         vkGetPhysicalDeviceProperties(device, &deviceProperties); 

         std::cout << std::endl; 
         std::cout <<"Device name: "<< deviceProperties.deviceName 
                   << std::endl; 

         if (isDeviceSuitable(device, surface)) 
               physicalDevice = device; 

   break; 

   } 

   if (physicalDevice == VK_NULL_HANDLE) { 
         throw std::runtime_error("failed to find suitable GPU !"); 
   } 

} 

在这里,我们创建一个int32来存储物理设备的数量。我们使用vkEnumeratePhysicalDevices获取可用的 GPU 数量,并将 Vulkan 实例、计数和第三个参数的null传递过去。这将检索可用的设备数量。如果deviceCount为零,这意味着没有可用的 GPU。然后,我们将可用的设备数量打印到控制台。

要获取物理设备本身,我们创建一个名为devices的向量,它将存储VkPhysicalDevice数据类型;这将为我们存储设备。我们将再次调用vkEnumeratePhysicalDevices函数,但这次——除了传递 Vulkan 实例和设备计数之外——我们还将设备信息存储在我们传递的第三个参数中。然后,我们将打印出带有DEVICE PROPERTIES标题的设备数量。

要获取可用设备的属性,我们将遍历设备数量,并使用vkGetPhysicalDeviceProperties获取它们的属性,在将它们存储在VkPhysicalDeviceProperties类型的变量中之前。

现在,我们需要打印出设备的名称,并在设备上调用DeviceSuitable。如果设备合适,我们将将其存储为physicalDevice并退出循环。请注意,我们将第一个可用的设备设置为我们将要使用的设备。

如果没有合适的设备,我们将抛出一个运行时错误,表示未找到合适的设备。让我们看看DeviceSuitable函数:

bool Device::isDeviceSuitable(VkPhysicalDevice device, VkSurfaceKHR 
   surface)  { 

   // find queue families the device supports 

   QueueFamilyIndices qFamilyIndices = findQueueFamilies(device, 
                                       surface); 

   // Check device extentions supported 
   bool extensionSupported = checkDeviceExtensionSupported(device); 

   bool swapChainAdequate = false; 

   // If swapchain extension is present  
   // Check surface formats and presentation modes are supported 
   if (extensionSupported) { 

         swapchainSupport = querySwapChainSupport(device, surface); 
         swapChainAdequate = !swapchainSupport.surfaceFormats.empty() 
                             && !swapchainSupport.presentModes.empty(); 

   } 

   VkPhysicalDeviceFeatures supportedFeatures; 
   vkGetPhysicalDeviceFeatures(device, &supportedFeatures); 

   return qFamilyIndices.arePresent() && extensionSupported && 
     swapChainAdequate && supportedFeatures.samplerAnisotropy; 

} 

在这个函数中,我们通过调用findQueueFamilies获取队列家族索引。然后,我们检查是否支持VK_KHR_SWAPCHAIN_EXTENSION_NAMEextension。之后,我们检查设备上的 swapchain 支持。如果表面格式和呈现模式不为空,swapChainAdequateboolean设置为true。最后,我们通过调用vkGetPhysicalDeviceFeatures获取物理设备特性。

最后,如果队列家族存在,swapchain 扩展被支持,swapchain 足够,并且设备支持各向异性过滤,我们将返回true。各向异性过滤是一种使远处的像素更清晰的模式。

各向异性过滤是一种模式,当启用时,有助于从极端角度查看的纹理变得更加清晰。

在以下示例中,右侧的图像启用了各向异性过滤,而左侧的图像未启用。在右侧的图像中,白色虚线在道路下方仍然相对可见。然而,在左侧的图像中,虚线变得模糊且像素化。因此,需要各向异性过滤:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/b63e98b4-c702-4646-82ba-fd69536d5b56.png

(摘自i.imgur.com/jzCq5sT.jpg)

让我们看看在上一函数中调用的三个函数。首先,让我们看看findQueueFamilies函数:

QueueFamilyIndicesDevice::findQueueFamilies(VkPhysicalDevicedevice, VkSurfaceKHRsurface) { 

   uint32_t queueFamilyCount = 0; 

   vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, 
      nullptr); 

   std::vector<VkQueueFamilyProperties>queueFamilies(queueFamilyCount); 

   vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, 
      queueFamilies.data()); 

   int i = 0; 

   for (constauto& queueFamily : queueFamilies) { 

         if (queueFamily.queueCount > 0 && queueFamily.queueFlags 
           &VK_QUEUE_GRAPHICS_BIT) { 
               queueFamiliyIndices.graphicsFamily = i; 
         } 

         VkBool32 presentSupport = false; 
         vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, 
           &presentSupport); 

         if (queueFamily.queueCount > 0 && presentSupport) { 
               queueFamiliyIndices.presentFamily = i; 
         } 

         if (queueFamiliyIndices.arePresent()) { 
               break; 
         } 

         i++; 
   } 

   return queueFamiliyIndices; 
}

要获取队列家族属性,我们调用vkGetPhysicalDeviceQueueFamilyProperties函数;然后,在物理设备中,我们传递一个int,我们用它来存储队列家族的数量,以及null指针。这将给我们提供可用的队列家族数量。

接下来,对于属性本身,我们创建了一个VkQueueFamilyProperties类型的向量,称为queueFamilies,用于存储必要的信息。然后,我们调用vkGetPhysicalDeviceFamilyProperties并传递物理设备、计数和queueFamilies本身,以填充所需的数据。我们创建一个inti,并将其初始化为0。这将存储图形和演示索引的索引。

for循环中,我们检查每个队列家族是否支持图形队列,通过查找VK_QUEUE_GRAPHICS_BIT。如果支持,我们设置图形家族索引。

然后,我们通过传递索引来检查演示支持。这将检查是否相同的家族也支持演示。如果它支持演示,我们将presentFamily设置为该索引。

如果队列家族支持图形和演示,图形和演示索引将是相同的。

以下截图显示了按设备划分的队列家族数量以及每个队列家族中的队列数量:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/b1fb0547-fff6-4569-aaa7-9d19500f67cc.png

我的 GPU 上有三个队列家族。第一个队列家族在 0^(th)索引处有 16 个队列,第二个队列家族在 1^(st)索引处有一个队列,第三个队列家族在 2^(nd)索引处有八个队列。

queueFlags指定队列家族中的队列。支持的队列可以是图形、计算、传输或稀疏绑定。

然后,我们检查是否找到了图形和显示索引,然后退出循环。最后,我们返回queueFamilyIndices。我在 Intel Iris Plus Graphics 650 上运行项目。这个集成的英特尔 GPU 有一个支持图形和显示队列的队列家族。不同的 GPU 有不同的队列家族,每个家族可能支持多种队列类型。接下来,让我们看看支持的设备扩展。我们可以通过使用checkDeviceExtensionSupported函数来检查这一点,该函数接受一个物理设备,如下面的代码所示:

 boolDevice::checkDeviceExtensionSupported(VkPhysicalDevicedevice){ 

   uint32_t extensionCount; 

   // Get available device extentions count 
   vkEnumerateDeviceExtensionProperties(device, nullptr, 
     &extensionCount, nullptr); 

   // Get available device extentions 
   std::vector<VkExtensionProperties>availableExtensions(extensionCount); 

   vkEnumerateDeviceExtensionProperties(device, nullptr,  
     &extensionCount, availableExtensions.data()); 

   // Populate with required device exentions we need 
   std::set<std::string>requiredExtensions(deviceExtensions.begin(), 
     deviceExtensions.end()); 

   // Check if the required extention is present 
   for (constauto& extension : availableExtensions) { 
         requiredExtensions.erase(extension.extensionName); 
   } 

   // If device has the required device extention then return  
   return requiredExtensions.empty(); 
} 

通过调用vkEnumerateDeviceExtensionProperties并传递物理设备、空指针、一个用于存储计数的int和一个空指针来获取设备支持的扩展数量。实际的属性存储在availableExtensions向量中,该向量存储VkExtensionProperties数据类型。通过再次调用vkEnumerateDeviceExtensionProperties,我们获取设备的扩展属性。

我们将所需的扩展添加到requiredExtensions向量中。然后,我们使用所需的扩展检查可用的扩展向量。如果找到所需的扩展,我们就从向量中移除它。这意味着设备支持该扩展,并从函数返回值,如下面的代码所示:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/c7f60c53-6246-4e52-a334-8763320c8bcd.png

运行在我设备上的设备有 73 个可用的扩展,如下面的代码所示。你可以设置一个断点并查看设备扩展属性以查看设备的支持扩展。我们将要查看的第三个函数是querySwapChainSupport函数,它填充了可用的表面功能、表面格式和显示模式:

SwapChainSupportDetailsDevice::querySwapChainSupport
   (VkPhysicalDevicedevice, VkSurfaceKHRsurface) { 

   SwapChainSupportDetails details; 

   vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, 
      &details.surfaceCapabilities); 

   uint32_t formatCount; 
   vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, 
      nullptr); 

   if (formatCount != 0) { 
         details.surfaceFormats.resize(formatCount); 
         vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, 
            &formatCount, details.surfaceFormats.data()); 
   } 

   uint32_t presentModeCount; 
   vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, 
     &presentModeCount, nullptr); 

   if (presentModeCount != 0) { 

         details.presentModes.resize(presentModeCount); 
         vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, 
           &presentModeCount, details.presentModes.data()); 
   } 

   return details; 
} 

要获取表面功能,我们调用vkGetPhysicalDeviceSurfaceCapabilitiesKHR并将设备(即surface)传递给它以获取表面功能。要获取表面格式和显示模式,我们分别调用vkGetPhysicalDeviceSurfaceFormatKHRvkGetPhysicalDeviceSurfacePresentModeKHR两次。

第一次调用vkGetPhysicalDeviceSurfacePresentModeKHR函数时,我们获取现有格式和模式的数量;我们第二次调用它以获取已填充并存储在结构体向量的格式和模式。

这里是我的设备表面的功能:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/c5c9f1c3-30f8-4dea-8b8f-cc6facc73daf.png

因此,最小图像计数是两个,这意味着我们可以添加双缓冲。以下是我的设备支持的表面格式和色彩空间:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/8887e334-8668-4c48-9e42-8831c766f2b0.png

这里是我的设备支持的显示模式:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/76276022-dd84-42d6-a807-7110aa899c6e.png

因此,我的设备似乎只支持即时模式。我们将在后续章节中看到它的用法。在获取物理设备属性后,我们为queueFamiliyIndices设置 getter 函数,如下所示:

QueueFamilyIndicesDevice::getQueueFamiliesIndicesOfCurrentDevice() { 

   return queueFamiliyIndices; 
} 

现在,我们可以使用createLogicalDevice函数创建逻辑设备。

要创建逻辑设备,我们必须填充VkDeviceCreateInfo结构体,这需要queueCreateInfo结构体。让我们开始吧:

  1. 创建一个向量,以便我们可以存储VkDeviceQueueCreateInfo和图形和呈现队列所需的任何信息。

  2. 创建另一个int类型的向量,以便我们可以存储图形和呈现队列的索引。

  3. 对于每个队列家族,填充VkDeviceQueueCreateInfo。创建一个局部结构体,传入结构体类型、队列家族索引、队列计数和优先级(为1),然后将它推入queueCreateInfos向量,如下所示:

void Device::createLogicalDevice(VkSurfaceKHRsurface, boolisValidationLayersEnabled, AppValidationLayersAndExtensions *appValLayersAndExtentions) { 

   // find queue families like graphics and presentation 
   QueueFamilyIndices indices = findQueueFamilies(physicalDevice, 
          surface); 

   std::vector<VkDeviceQueueCreateInfo> queueCreateInfos; 

   std::set<int> uniqueQueueFamilies = { indices.graphicsFamily, 
                                       indices.presentFamily }; 

   float queuePriority = 1.0f; 

   for (int queueFamily : uniqueQueueFamilies) { 

         VkDeviceQueueCreateInfo queueCreateInfo = {}; 
         queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE
                                 _QUEUE_CREATE_INFO; 
         queueCreateInfo.queueFamilyIndex = queueFamily; 
         queueCreateInfo.queueCount = 1; // we only require 1 queue 
         queueCreateInfo.pQueuePriorities = &queuePriority; 
         queueCreateInfos.push_back(queueCreateInfo); 
   } 
  1. 要创建设备,指定我们将使用的设备功能。对于设备功能,我们将创建一个VkPhysicalDeviceFeatures类型的变量,并将samplerAnisotropy设置为true,如下所示:
 //specify device features  
   VkPhysicalDeviceFeatures deviceFeatures = {};  

   deviceFeatures.samplerAnisotropy = VK_TRUE; 

  1. 创建VkDeviceCreateInfo结构体,这是创建逻辑设备所必需的。将类型设置为VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,然后设置queueCreateInfos、计数和要启用的设备功能。

  2. 设置设备扩展计数和名称。如果启用了验证层,我们设置验证层的计数和名称。通过调用vkCreateDevice并传入物理设备、创建设备信息和null分配器来创建logicalDevice。然后,创建逻辑设备,如下所示。如果失败,则抛出运行时错误:

   VkDeviceCreateInfo createInfo = {}; 
   createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; 
   createInfo.pQueueCreateInfos = queueCreateInfos.data(); 
   createInfo.queueCreateInfoCount = static_cast<uint32_t>
                                     (queueCreateInfos.size()); 

   createInfo.pEnabledFeatures = &deviceFeatures; 
   createInfo.enabledExtensionCount = static_cast<uint32_t>
     (deviceExtensions.size()); 
   createInfo.ppEnabledExtensionNames = deviceExtensions.data(); 

   if (isValidationLayersEnabled) { 
      createInfo.enabledLayerCount = static_cast<uint32_t>(appValLayersAndExtentions->requiredValidationLayers.size()); 
      createInfo.ppEnabledLayerNames = appValLayersAndExtentions->
                               requiredValidationLayers.data(); 
   } 
   else { 
         createInfo.enabledLayerCount = 0; 
   } 

   //create logical device 

   if (vkCreateDevice(physicalDevice, &createInfo, nullptr, 
      &logicalDevice) != VK_SUCCESS) { 
         throw std::runtime_error("failed to create logical 
            device !"); 
   }
  1. 获取设备图形和呈现队列,如下所示。我们现在完成了Device类的操作:
//get handle to the graphics queue of the gpu 
vkGetDeviceQueue(logicalDevice, indices.graphicsFamily, 0, 
&graphicsQueue); 

//get handle to the presentation queue of the gpu 
vkGetDeviceQueue(logicalDevice, indices.presentFamily, 0, &presentQueue); 

}  
  1. 这完成了Device类的封装。在VulkanContext.h文件中包含Device.h文件,并在VulkanContext类的私有部分添加一个新的Device类型设备对象,如下所示:
// My Classes
   AppValidationLayersAndExtensions *valLayersAndExt; 
   VulkanInstance* vInstance; 
   Device* device; 
  1. VulkanContext.cpp文件中的VulkanInit函数中,在创建表面之后添加以下代码:
device = new Device(); 
device->pickPhysicalDevice(vInstance, surface); 
device->createLogicalDevice(surface, isValidationLayersEnabled,
   valLayersAndExt);   
  1. 这将创建device类的新实例,并从可用的物理设备中选择一个设备。然后,你将能够创建逻辑设备。运行应用程序以查看应用程序将在哪个设备上运行。在我的台式机上,找到了以下设备计数和名称:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/ba1c7ca6-5f59-48e6-81fb-e3ab6cc5abf2.png

  1. 在我的笔记本电脑上,应用程序找到了以下设备名称的设备:

https://github.com/OpenDocCN/freelearn-c-cpp-zh/raw/master/docs/cpp-gmdev-ex/img/2c89d883-db8b-4d79-97d1-43caacc381a8.png

  1. findQueueFamiliescheckDeviceExtensionSupportquerySwapChainSupport函数内部设置断点,以检查队列家族设备扩展的数量以及 GPU 对 swapchain 的支持情况。

摘要

我们已经完成了大约四分之一的渲染到视口的过程。在这一章中,我们设置了验证层和我们需要设置的扩展,以便设置 Vulkan 渲染。我们创建了一个 Vulkan 应用程序和实例,然后创建了一个设备类,以便我们可以选择物理设备。我们还创建了一个逻辑设备,以便我们可以与 GPU 交互。

在下一章中,我们将创建 swapchain 本身,以便我们可以在缓冲区之间进行交换,并且我们将创建渲染和深度纹理来绘制场景。我们将创建一个渲染通道来设置渲染纹理的使用方式,然后创建绘制命令缓冲区,这些缓冲区将执行我们的绘制命令。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值