C++ 秘籍:问题解决方法(七)
十四、3D 图形编程
C++ 是高性能应用程序开发人员的首选编程语言。这通常包括需要向用户显示 3D 图形的应用程序。3D 图形在医疗应用、设计应用和视频游戏中很常见。所有这些类型的应用程序都要求将响应能力作为一个关键的可用性特性。这使得 C++ 语言成为这类程序的完美选择,因为程序员可以针对特定的硬件平台进行优化。
微软为 Windows 操作系统提供构建 3D 应用程序的专有 DirectX API。然而,本章着眼于使用 OpenGL API 编写一个简单的 3D 程序。Windows、OS X 和大多数 Linux 发行版都支持 OpenGL 在这种情况下,这是一个完美的选择,因为您可能会使用这些操作系统中的任何一个。
OpenGL 编程的一个比较乏味的方面是,如果你的目标不止一个,就需要在多个操作系统中设置和管理窗口。GLFW 包使这项工作变得更加容易,它将这项任务抽象在一个 API 后面,因此您不必担心细节。
14-1.GLFW 简介
问题
您正在编写一个包含 3D 图形的跨平台应用程序,并且您想要一种快速启动和运行的方法。
解决办法
GLFW 抽象出为许多流行的操作系统创建和管理窗口的任务。
它是如何工作的
GLFW API 是用 C 编程语言编写的,因此可以毫无问题地在 C++ 应用程序中使用。该 API 可从www.glfw.org下载。您还可以在同一网站上阅读 API 的文档。配置和构建 GLFW 库的说明经常变化,因此本章不包括这些说明。在撰写本文时,可以在www.glfw.org/docs/latest/compile.html找到构建 GLFW 的最新说明。
GLFW 的说明目前涉及使用 CMake 构建一个项目,然后可以使用该项目编译一个库,您可以将该库链接到您自己的项目中。一旦你建立并运行了这个,你就可以使用清单 14-1 中的代码来运行一个初始化 OpenGL 的程序,并为你的程序创建一个窗口。
清单 14-1 。一个简单的 GLFW 程序
#include "GLFW\glfw3.h"
int*main()*
{
GLFWwindow* window;
/* Initialize the library */
if (!glfwInit())
return -1;
/* Create a windowed mode window and its OpenGL context */
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
/* Make the window's context current */
glfwMakeContextCurrent(window);
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
/* Render here */
/* Swap front and back buffers */
glfwSwapBuffers(window);
/* Poll for and process events */
glfwPollEvents();
}
glfwTerminate();
return 0;
}
清单 14-1 中的代码是 GLFW 网站上提供的示例程序,用于确保您的构建工作正常。它通过调用glfwInit来初始化glfw库。使用glfwCreateWindow功能创建一个窗口。该示例创建一个分辨率为 640 × 480、标题为“Hello World”的窗口。如果窗口创建失败,则调用glfwTerminate函数。如果成功,程序调用glfwMakeContextCurrent。OpenGL API 支持多个渲染上下文,当您想要渲染时,您必须确保您的上下文是当前上下文。程序的main循环继续,直到glfwWindowShouldClose函数返回true。glfwSwapBuffers功能负责用后台缓冲交换前台缓冲。双缓冲渲染有助于防止用户看到未完成的动画帧。当程序渲染到第二个缓冲区时,图形卡可以显示一个缓冲区。这些缓冲区在每帧结束时交换。glfwPollEvents函数负责与操作系统通信并接收任何消息。程序以调用glfwTerminate关闭所有东西结束。
OpenGL API 通过扩展提供了许多功能,这意味着您正在使用的功能可能不被您正在工作的平台直接支持。幸运的是,GLEW 库可以帮助在多种平台上使用 OpenGL 扩展。同样,获取、构建和链接该库的说明会随时发生变化。最新信息可以在http://glew.sourceforge.net从 GLEW 网站获得。
一旦你启动并运行了 GLEW,你可以使用清单 14-2 中的glewInit函数调用来初始化这个库。
清单 14-2 。正在初始化 GLEW
#include <GL/glew.h>
#include "GLFW/glfw3.h"
int main(void)
{
GLFWwindow* window;
// Initialize the library
if (!glfwInit())
{
return -1;
}
// Create a windowed mode window and its OpenGL context
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
// Make the window's context current
glfwMakeContextCurrent(window);
GLenum glewError{ glewInit() };
if (glewError != GLEW_OK)
{
return -1;
}
// Loop until the user closes the window
while (!glfwWindowShouldClose(window))
{
// Swap front and back buffers
glfwSwapBuffers(window);
// Poll for and process events
glfwPollEvents();
}
glfwTerminate();
return 0;
}
重要的是,这一步发生在你有一个有效的和当前的 OpenGL 上下文之后,因为 GLEW 库依赖于此来从 OpenGL API 加载你可能正在使用的最常见的扩展。
本书附带了一些示例应用程序,其中包含并配置了 GLEW 和 GLFW。如果您想查看已配置为使用这些库的项目,您应该下载这些库。此外,在库的网站(http://glew.sourceforge.net/install.html和www.glfw.org/download.html)上可以找到优秀的文献。
14-2.渲染一个三角形
问题
您希望在应用程序中呈现 3D 对象。
解决办法
OpenGL 提供 API 来配置图形卡上的渲染管道,并在屏幕上显示 3D 对象。
它是如何工作的
OpenGL 是一个图形库,允许应用程序向计算机中的 GPU 发送数据,以将图像渲染到窗口中。这份食谱向你介绍了在现代计算机系统上使用 OpenGL 时将图形渲染到窗口所必需的三个概念。首先是几何的概念。
对象的几何由顶点和索引的集合组成。顶点指定空间中顶点应该呈现在屏幕上的点。一个顶点通过 GPU,在不同的点对它应用不同的操作。这个方法绕过了顶点的大部分处理,而是在所谓的normalized device coordinates中指定顶点。GPU 使用顶点着色器来转换顶点,以生成位于规范化立方体内的顶点。然后将这些顶点传递给片段着色器,片段用于确定在给定点写入帧缓冲区的输出颜色。当你阅读本章的食谱时,你会学到更多关于这些操作的知识。
清单 14-3 中的代码展示了Geometry类以及如何使用它来指定顶点和索引的存储。
清单 14-3 。Geometry Class
using namespace std;
class Geometry
{
public:
using Vertices = vector < float >;
using Indices = vector < unsigned short >;
private:
Vertices m_Vertices;
Indices m_Indices;
public:
Geometry() = default;
~Geometry() = default;
void SetVertices(const Vertices& vertices)
{
m_Vertices = vertices;
}
Vertices::size_type GetNumVertices() const
{
return m_Vertices.size();
}
Vertices::const_pointer GetVertices() const
{
return m_Vertices.data();
}
void SetIndices(const Indices& indices)
{
m_Indices = indices;
}
Indices::size_type GetNumIndices() const
{
return m_Indices.size();
}
Indices::const_pointer GetIndices() const
{
return m_Indices.data();
}
};
Geometry类包含两个向量别名。第一个别名用于定义代表float s 的vector的类型。该类型用于存储Geometry类中的顶点。第二个类型别名定义了一个unsigned short s 的vector,该类型别名用于创建用于存储索引的m_Indices向量。
使用 OpenGL 时,索引是一个有用的工具,因为它们允许您减少顶点数据中的重复顶点。网格通常由一组三角形组成,每个三角形都与其他三角形共享边,以创建一个没有任何洞的完整形状。这意味着不在对象边缘的单个顶点在多个三角形之间共享。使用索引可以创建网格的所有顶点,然后使用索引来表示 OpenGL 读取顶点以创建网格的各个三角形的顺序。你会在这个配方的后面看到顶点和索引的定义。
典型的 OpenGL 程序由多个着色器程序组成。着色器允许您控制 OpenGL 渲染管道的多个阶段的行为。此时,您需要能够创建一个顶点着色器和一个片段着色器,它们可以作为 GPU 的单个管道。OpenGL 通过让您独立创建顶点着色器和片段着色器并将它们链接到单个着色器程序中来实现这一点。你通常有不止一个这样的类,所以清单 14-4 中的Shader基类展示了如何创建一个在多个派生着色器程序中共享的基类。
清单 14-4 。Shader类
class Shader
{
private:
void LoadShader(GLuint id, const std::string& shaderCode)
{
const unsigned int NUM_SHADERS{ 1 };
const char* pCode{ shaderCode.c_str() };
GLint length{ static_cast<GLint>(shaderCode.length()) };
glShaderSource(id, NUM_SHADERS, &pCode, &length);
glCompileShader(id);
glAttachShader(m_ProgramId, id);
}
protected:
GLuint m_VertexShaderId{ GL_INVALID_VALUE };
GLuint m_FragmentShaderId{ GL_INVALID_VALUE };
GLint m_ProgramId{ GL_INVALID_VALUE };
std::string m_VertexShaderCode;
std::string m_FragmentShaderCode;
public:
Shader() = default;
virtual ~Shader() = default;
virtual void Link()
{
m_ProgramId = glCreateProgram();
m_VertexShaderId = glCreateShader(GL_VERTEX_SHADER);
LoadShader(m_VertexShaderId, m_VertexShaderCode);
m_FragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
LoadShader(m_FragmentShaderId, m_FragmentShaderCode);
glLinkProgram(m_ProgramId);
}
virtual void Setup(const Geometry& geometry)
{
glUseProgram(m_ProgramId);
}
};
Shader类是你第一次看到 OpenGL API 的使用。该类包含用于存储 OpenGL 提供的 id 的变量,这些 id 充当顶点和片段着色器以及着色器程序的句柄。当m_ProgramId字段被赋予glCreateProgram方法的结果时,它在Link方法中被初始化。m_VertexShaderId被赋予glCreateShader程序的值,该值被传递给GL_VERTEX_SHADER变量。使用同一个变量初始化了m_FragmentShaderId变量,但是它传递给了GL_FRAGMENT_SHADER变量。您可以使用LoadShader方法为顶点着色器或片段着色器加载着色器代码。当在Link方法中两次调用LoadShader方法时,您可以看到这一点:第一次使用m_VertexShaderId和m_VertexShaderCode变量作为参数,第二次使用m_FragmentShaderId和m_FragentShaderCode变量。Link方法以调用glLinkProgram结束。
LoadShader方法负责将着色器源代码附加到着色器 ID,编译着色器,并将其附加到相关的 OpenGL 着色器程序。Setup方法在渲染对象时使用,它告诉 OpenGL 你想让这个着色器程序成为使用中的活动着色器。这个配方需要一个着色器程序在屏幕上渲染一个三角形。这个着色器程序是通过从清单 14-4 中的Shader类派生出一个名为BasicShader的类来创建的,如清单 14-5 所示。
清单 14-5 。BasicShader类
class BasicShader
: public Shader
{
private:
GLint m_PositionAttributeHandle;
public:
BasicShader()
{
m_VertexShaderCode =
"attribute vec4 a_vPosition; \n"
"void main(){ \n"
" gl_Position = a_vPosition; \n"
"} \n";
m_FragmentShaderCode =
"#version 150 \n"
"precision mediump float; \n"
"void main(){ \n"
" gl_FragColor = vec4(0.2, 0.2, 0.2, 1.0); \n"
"} \n";
}
~BasicShader() override = default;
void Link() override
{
Shader::Link();
GLint success;
glGetProgramiv(m_ProgramId, GL_ACTIVE_ATTRIBUTES, &success);
m_PositionAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vPosition");
}
void Setup(const Geometry& geometry) override
{
Shader::Setup(geometry);
glVertexAttribPointer(
m_PositionAttributeHandle,
3,
GL_FLOAT,
GL_FALSE,
0,
geometry.GetVertices());
glEnableVertexAttribArray(m_PositionAttributeHandle);
}
};
BasicShader类从在其构造函数中初始化来自Shader类的受保护的m_VertexShaderCode和m_FragmentShaderCode变量开始。Link方法负责调用基类Link方法,然后检索着色器代码中属性的句柄。Setup方法还调用基类中的Setup方法。然后,它在着色器程序中设置属性。属性 是一个变量,它从应用程序代码中使用 OpenGL API 函数设置的数据流或字段中接收数据。在这种情况下,属性是 GL 着色语言(GLSL) 代码中的vec4字段。GLSL 用于编写 OpenGL 着色器代码;这种语言是基于 C 的,因此很熟悉,但是它包含了自己的类型和与应用程序端 OpenGL 调用进行通信所必需的关键字。顶点着色器代码中的a_vPosition vec4属性负责接收发送给 OpenGL 进行渲染的顶点流中的每个位置。使用glGetAttribLocation OpenGL API 函数检索属性的句柄,该函数获取程序 ID 和要检索的属性的名称。顶点位置的属性句柄可以与Setup方法中的glVertexAttribPointer函数一起使用。该方法将属性句柄作为参数,后跟每个顶点的元素数量。在这种情况下,顶点由 x,y,z 分量提供;因此,数字 3 被传递给size参数。The GL_FLOAT值指定顶点是浮点型的。GL_FALSE告诉 OpenGL,当 API 接收到顶点时,不应将其规范化。0 值告诉 OpenGL 顶点数据位置之间的间隙大小;在这种情况下,没有间隙,所以可以传递 0。最后,提供一个指向顶点数据的指针。在这个函数调用之后,调用glEnableVertexAttribArray函数来告诉 OpenGL 应该使用在之前的调用中提供给它的数据来启用该属性,以便向 GPU 上的顶点着色器执行系统提供位置数据。
下一步是在main函数中使用这些类来渲染一个三角形到你的窗口。清单 14-6 包含了实现这一点的程序的完整清单。
清单 14-6 。渲染三角形的程序
#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include <string>
#include <vector>
using namespace std;
class Geometry
{
public:
using Vertices = vector < float >;
using Indices = vector < unsigned short >;
private:
Vertices m_Vertices;
Indices m_Indices;
public:
Geometry() = default;
~Geometry() = default;
void SetVertices(const Vertices& vertices)
{
m_Vertices = vertices;
}
Vertices::size_type GetNumVertices() const
{
return m_Vertices.size();
}
Vertices::const_pointer GetVertices() const
{
return m_Vertices.data();
}
void SetIndices(const Indices& indices)
{
m_Indices = indices;
}
Indices::size_type GetNumIndices() const
{
return m_Indices.size();
}
Indices::const_pointer GetIndices() const
{
return m_Indices.data();
}
};
class Shader
{
private:
void LoadShader(GLuint id, const std::string& shaderCode)
{
const unsigned int NUM_SHADERS{ 1 };
const char* pCode{ shaderCode.c_str() };
GLint length{ static_cast<GLint>(shaderCode.length()) };
glShaderSource(id, NUM_SHADERS, &pCode, &length);
glCompileShader(id);
glAttachShader(m_ProgramId, id);
}
protected:
GLuint m_VertexShaderId{ GL_INVALID_VALUE };
GLuint m_FragmentShaderId{ GL_INVALID_VALUE };
GLint m_ProgramId{ GL_INVALID_VALUE };
std::string m_VertexShaderCode;
std::string m_FragmentShaderCode;
public:
Shader() = default;
virtual ~Shader() = default;
virtual void Link()
{
m_ProgramId = glCreateProgram();
m_VertexShaderId = glCreateShader(GL_VERTEX_SHADER);
LoadShader(m_VertexShaderId, m_VertexShaderCode);
m_FragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
LoadShader(m_FragmentShaderId, m_FragmentShaderCode);
glLinkProgram(m_ProgramId);
}
virtual void Setup(const Geometry& geometry)
{
glUseProgram(m_ProgramId);
}
};
class BasicShader
: public Shader
{
private:
GLint m_PositionAttributeHandle;
public:
BasicShader()
{
m_VertexShaderCode =
"attribute vec4 a_vPosition; \n"
"void main(){ \n"
" gl_Position = a_vPosition; \n"
"} \n";
m_FragmentShaderCode =
"#version 150 \n"
"precision mediump float; \n"
"void main(){ \n"
" gl_FragColor = vec4(0.2, 0.2, 0.2, 1.0); \n"
"} \n";
}
~BasicShader() override = default;
void Link() override
{
Shader::Link();
m_PositionAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vPosition");
}
void Setup(const Geometry& geometry) override
{
Shader::Setup(geometry);
glVertexAttribPointer(
m_PositionAttributeHandle,
3,
GL_FLOAT,
GL_FALSE,
0,
geometry.GetVertices());
glEnableVertexAttribArray(m_PositionAttributeHandle);
}
};
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
)
{
GLFWwindow* window;
// Initialize the library
if (!glfwInit())
{
return -1;
}
// Create a windowed mode window and its OpenGL context
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
// Make the window's context current
glfwMakeContextCurrent(window);
GLenum glewError{ glewInit() };
if (glewError != GLEW_OK)
{
return -1;
}
BasicShader basicShader;
basicShader.Link();
Geometry triangle;
Geometry::Vertices vertices{
0.0f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f
};
Geometry::Indices indices{ 0, 1, 2 };
triangle.SetVertices(vertices);
triangle.SetIndices(indices);
glClearColor(0.25f, 0.25f, 0.95f, 1.0f);
// Loop until the user closes the window
while (!glfwWindowShouldClose(window))
{
glClear(GL_COLOR_BUFFER_BIT);
basicShader.Setup(triangle);
glDrawElements(GL_TRIANGLES,
triangle.GetNumIndices(),
GL_UNSIGNED_SHORT,
triangle.GetIndices());
// Swap front and back buffers
glfwSwapBuffers(window);
// Poll for and process events
glfwPollEvents();
}
glfwTerminate();
return 0;
}
清单 14-6 中的main函数展示了如何以及在哪里使用Geometry和BasicShader类来渲染一个三角形到你的窗口。在对glewInit 的调用成功完成后,OpenGL API 立即可用。在这个调用之后,main函数初始化一个BasicShader对象,然后调用BasicShader::Link和Geometry对象来表示一个三角形的顶点。顶点以后变换状态提供,因为BasicShader中的顶点着色器不对传递的数据执行任何操作。顶点在标准化的设备坐标中指定;在 OpenGL 中,这些坐标必须放在一个立方体中,对于 x、y 和 z 坐标,立方体的范围从-1、-1、-1 到 1,1,1。索引告诉 OpenGL 将顶点传递给顶点着色器的顺序;在这种情况下,您将按照定义的顺序传递顶点。
glClearColor函数告诉 OpenGL 当没有其他像素被渲染到该位置时,用来表示背景颜色的颜色。这里颜色被设置为浅蓝色,所以很容易判断像素何时被渲染。在 OpenGL 中,颜色由四个部分表示:红色、绿色、蓝色和 alpha。红色、绿色和蓝色分量组合起来生成像素的颜色。当所有分量值都为 1 时,颜色为白色;当所有值都为 0 时,颜色为黑色。alpha 分量用于确定像素的透明度。几乎没有理由将背景色的透明度值设置为小于 1。
你可以在渲染循环中找到对glClear的调用。该调用使用由glClearColor设置的值来填充framebuffer,并覆盖上次使用该缓冲区时呈现的任何内容。请记住,当您使用双缓冲时,您渲染到的缓冲是两帧之前的,而不是一帧。BasicShader::Setup函数用当前几何图形设置着色器进行渲染。在这个程序中,这可能是一次性的操作,但是对于程序来说,用给定的着色器渲染多个对象更为常见。
最后,glDrawElements函数负责要求 OpenGL 渲染三角形。glDrawElements 调用指定您想要呈现三角形图元、要呈现的索引数量、索引类型以及指向索引数据流的指针。
图 14-1 显示了该程序生成的输出。
图 14-1 。清单 14-6 中的代码呈现的三角形
14-3.创建一个纹理四边形
问题
GPU 的能力是有限的,你想给你的对象一个更详细的外观。
解决办法
纹理映射允许您创建 2D 图像,您可以在网格表面上映射这些图像,以增加几何复杂性的外观。
它是如何工作的
GLSL 提供了对采样器的支持,可以用来从指定的纹理中读取纹理元素。一个纹理元素 是来自纹理的单一颜色元素;这个术语是纹理元素的简称,就像像素是图片元素的简称一样。术语像素 通常用于指构成显示器上图像的单个颜色,而纹理像素用于指纹理图像中的单个颜色。
使用纹理坐标将纹理映射到网格。网格中的每个顶点都有一个关联的纹理坐标,您可以使用该坐标在片段着色器中查找要应用于片段的颜色。使用 GPU 上的插值器单元将每个顶点的纹理坐标插值到多边形的表面上。要从顶点着色器传递到片段着色器的插值在 OpenGL 中使用varying关键字表示。该关键字具有逻辑意义,因为varying用于表示在多边形表面上变化的变量。Varying在顶点着色器中,通过从属性分配或由代码生成来初始化。
您需要一种方法来表示包含纹理坐标的网格数据,然后才能考虑在应用程序中使用纹理。清单 14-7 显示了支持顶点数据中纹理坐标的Geometry类的定义。
清单 14-7 。一个支持纹理坐标的类
class Geometry
{
public:
using Vertices = vector < float >;
using Indices = vector < unsigned short >;
private:
Vertices m_Vertices;
Indices m_Indices;
unsigned int m_NumVertexPositionElements{};
unsigned int m_NumTextureCoordElements{};
unsigned int m_VertexStride{};
public:
Geometry() = default;
~Geometry() = default;
void SetVertices(const Vertices& vertices)
{
m_Vertices = vertices;
}
Vertices::size_type GetNumVertices() const
{
return m_Vertices.size();
}
Vertices::const_pointer GetVertices() const
{
return m_Vertices.data();
}
void SetIndices(const Indices& indices)
{
m_Indices = indices;
}
Indices::size_type GetNumIndices() const
{
return m_Indices.size();
}
Indices::const_pointer GetIndices() const
{
return m_Indices.data();
}
Vertices::const_pointer GetTexCoords() const
{
return static_cast<Vertices::const_pointer>(&m_Vertices[m_NumVertexPositionElements]);
}
void SetNumVertexPositionElements(unsigned int numVertexPositionElements)
{
m_NumVertexPositionElements = numVertexPositionElements;
}
unsigned int GetNumVertexPositionElements() const
{
return m_NumVertexPositionElements;
}
void SetNumTexCoordElements(unsigned int numTexCoordElements)
{
m_NumTextureCoordElements = numTexCoordElements;
}
unsigned int GetNumTexCoordElements() const
{
return m_NumTextureCoordElements;
}
void SetVertexStride(unsigned int vertexStride)
{
m_VertexStride = vertexStride;
}
unsigned int GetVertexStride() const
{
return m_VertexStride;
}
};
这段代码显示了在单独的vectors中存储顶点和索引的Geometry类的定义。还有存储顶点位置元素的数量和纹理坐标元素的数量的字段。单个顶点可以由可变数量的顶点元素和可变数量的纹理坐标组成。m_VertexStride字段存储从一个顶点开始到下一个顶点开始的字节数。GetTexCoords方法是这个类中比较重要的方法之一,因为它表明这个类支持的顶点数据是一个结构数组格式。读入顶点数据有两种主要方法:可以为单独数组中的顶点和纹理坐标设置单独的流,也可以设置一个单独的流来交错每个顶点的顶点位置和纹理坐标数据。这个类支持后一种风格,因为这是现代 GPU 的最佳数据格式。GetTexCoords方法使用m_NumVertexPositionElements作为查找数据的索引返回第一个纹理坐标的地址。这依赖于你的网格数据被紧密打包,并且你的第一个纹理坐标紧跟在顶点位置元素之后。
使用 OpenGL 渲染纹理对象时的下一个重要元素是一个可以从文件中加载纹理数据的类。TGA 文件格式简单易用,可以存储图像数据。它的简单性意味着当使用 OpenGL 时,它是未压缩纹理的一种常见的文件格式选择。清单 14-8 中的TGAFile类展示了如何加载一个 TGA 文件。
清单 14-8 。TGAFile Class
class TGAFile
{
private:
#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct TGAHeader
{
unsigned char m_IdSize{};
unsigned char m_ColorMapType{};
unsigned char m_ImageType{};
unsigned short m_PaletteStart{};
unsigned short m_PaletteLength{};
unsigned char m_PaletteBits{};
unsigned short m_XOrigin{};
unsigned short m_YOrigin{};
unsigned short m_Width{};
unsigned short m_Height{};
unsigned char m_BytesPerPixel{};
unsigned char m_Descriptor{};
}
#ifndef _MSC_VER
__attribute__ ((packed))
#endif // _MSC_VER
;
#ifdef _MSC_VER
#pragma pack(pop)
#endif
std::vector<char> m_FileData;
TGAHeader* m_pHeader{};
void* m_pImageData{};
public:
TGAFile(const std::string& filename)
{
std::ifstream fileStream{ filename, std::ios_base::binary };
if (fileStream.is_open())
{
fileStream.seekg(0, std::ios::end);
m_FileData.resize(static_cast<unsigned int>(fileStream.tellg()));
fileStream.seekg(0, std::ios::beg);
fileStream.read(m_FileData.data(), m_FileData.size());
fileStream.close();
m_pHeader = reinterpret_cast<TGAHeader*>(m_FileData.data());
m_pImageData = static_cast<void*>(m_FileData.data() + sizeof(TGAHeader));
}
}
unsigned short GetWidth() const
{
return m_pHeader->m_Width;
}
unsigned short GetHeight() const
{
return m_pHeader->m_Height;
}
unsigned char GetBytesPerPixel() const
{
return m_pHeader->m_BytesPerPixel;
}
unsigned int GetDataSize() const
{
return m_FileData.size() - sizeof(TGAHeader);
}
void* GetImageData() const
{
return m_pImageData;
}
};
TGAFile类包含一个 header 结构,它表示由图像编辑程序(如 Adobe Photoshop)保存时 TGA 文件中包含的标题数据。这个结构有一些与之相关的有趣的编译器元数据。现代 C++ 编译器知道应用程序中数据结构的内存布局。给定的 CPU 架构可以更有效地操作位于特定存储器边界上的变量。这对于不可移植且在单 CPU 架构上的单个程序中使用的结构来说没问题,但是对于由不同计算机上的不同程序保存和加载的数据来说,这可能会导致问题。为了抵消这一点,您可以指定编译器可以添加到程序中的填充量,以优化对单个变量的访问。TGAHeader结构要求不添加任何填充,因为保存文件时 TGA 文件格式不包含任何填充。这是在使用 Visual Studio 时通过使用pragma预处理器指令和pack命令将push和pop的打包值设为 1 来实现的。这将禁用变量的自动间距以提高速度效率。在大多数其他编译器上,您可以使用__attribute__ ((packed))编译器指令来获得相同的结果。
TGAHeader字段存储代表存储在文件中的图像数据类型的元数据。这个方法只处理 TGA 中的 RGBA 数据,所以唯一相关的字段是宽度、高度和每像素字节数。这些可以在文件中的TGAHeader结构中表示的精确字节位置中找到。通过使用指针,文件中的数据被映射到TGAHeader对象中。文件名被传递给该类的构造函数,然后使用一个ifstream对象打开并读取该文件。ifstream对象是为从文件中读入数据而提供的 STL 类。通过向其传递要打开的文件名和二进制数据模式来构造ifstream,因为您想要从文件中读取二进制数据。整个文件被读入一个由char个变量组成的向量,方法是查找到文件的末尾,读取文件末尾的位置以确定文件中数据的大小,然后返回到开头并使用大小来调整向量的大小。然后通过使用ifstream read方法将数据读入向量,该方法获取一个指向应该读取数据的缓冲区的指针和要读取的缓冲区的大小。然后,您可以使用reinterpret_cast将从文件中读取的数据映射到一个TGAHeader结构上,并且可以使用一个static_cast来存储指向图像数据开头的指针。
通过使用单独的类,加载 TGA 数据与 OpenGL 纹理设置是分开的。从 TGA 加载的数据可以传递给列表 14-9 中所示的纹理类来创建一个 OpenGL 纹理对象。
清单 14-9 。Texture Class
class Texture
{
private:
unsigned int m_Width{};
unsigned int m_Height{};
unsigned int m_BytesPerPixel{};
unsigned int m_DataSize{};
GLuint m_Id{};
void* m_pImageData;
public:
Texture(const TGAFile& tgaFile)
: Texture(tgaFile.GetWidth(),
tgaFile.GetHeight(),
tgaFile.GetBytesPerPixel(),
tgaFile.GetDataSize(),
tgaFile.GetImageData())
{
}
Texture(unsigned int width,
unsigned int height,
unsigned int bytesPerPixel,
unsigned int dataSize,
void* pImageData)
: m_Width(width)
, m_Height(height)
, m_BytesPerPixel(bytesPerPixel)
, m_DataSize(dataSize)
, m_pImageData(pImageData)
{
}
~Texture() = default;
GLuint GetId() const
{
return m_Id;
}
void Init()
{
GLint packBits{ 4 };
GLint internalFormat{ GL_RGBA };
GLint format{ GL_BGRA };
glGenTextures(1, &m_Id);
glBindTexture(GL_TEXTURE_2D, m_Id);
glPixelStorei(GL_UNPACK_ALIGNMENT, packBits);
glTexImage2D(GL_TEXTURE_2D,
0,
internalFormat,
m_Width,
m_Height,
0,
format,
GL_UNSIGNED_BYTE,
m_pImageData);
}
};
类初始化一个 OpenGL 纹理,在渲染对象时使用。提供这两个类构造函数是为了简化从 TGA 文件或内存数据初始化类。采用TGAFile引用的构造函数使用 C++11 委托构造函数的概念来调用内存中的构造函数。Init方法负责创建 OpenGL 纹理对象。此方法可以使用构造函数中提供的宽度和高度从 BGRA 源创建 RGBA 纹理。您可能会注意到,TGA 文件中的源像素是前后颠倒的;这种方法负责将红色和绿色通道转换到 GPU 的正确位置。图像数据被glTextImage2D函数复制到 GPU 上,这样draw调用可以在你的片段着色器中使用这些纹理数据。
能够使用纹理进行渲染的下一步是查看TextureShader类,它包括一个顶点着色器,可以读入纹理坐标,并通过一个变化的对象将它们传递给片段着色器。你可以在的清单 14-10 中看到这个类。
清单 14-10 。TextureShader Class
class Shader
{
private:
void LoadShader(GLuint id, const std::string& shaderCode)
{
const unsigned int NUM_SHADERS{ 1 };
const char* pCode{ shaderCode.c_str() };
GLint length{ static_cast<GLint>(shaderCode.length()) };
glShaderSource(id, NUM_SHADERS, &pCode, &length);
glCompileShader(id);
glAttachShader(m_ProgramId, id);
}
protected:
GLuint m_VertexShaderId{ GL_INVALID_VALUE };
GLuint m_FragmentShaderId{ GL_INVALID_VALUE };
GLint m_ProgramId{ GL_INVALID_VALUE };
std::string m_VertexShaderCode;
std::string m_FragmentShaderCode;
public:
Shader() = default;
virtual ~Shader() = default;
virtual void Link()
{
m_ProgramId = glCreateProgram();
m_VertexShaderId = glCreateShader(GL_VERTEX_SHADER);
LoadShader(m_VertexShaderId, m_VertexShaderCode);
m_FragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
LoadShader(m_FragmentShaderId, m_FragmentShaderCode);
glLinkProgram(m_ProgramId);
}
virtual void Setup(const Geometry& geometry)
{
glUseProgram(m_ProgramId);
}
};
class TextureShader
: public Shader
{
private:
const Texture& m_Texture;
GLint m_PositionAttributeHandle;
GLint m_TextureCoordinateAttributeHandle;
GLint m_SamplerHandle;
public:
TextureShader(const Texture& texture)
: m_Texture(texture)
{
m_VertexShaderCode =
"attribute vec4 a_vPosition; \n"
"attribute vec2 a_vTexCoord; \n"
"varying vec2 v_vTexCoord; \n"
" \n"
"void main() { \n"
" gl_Position = a_vPosition; \n"
" v_vTexCoord = a_vTexCoord; \n"
"} \n";
m_FragmentShaderCode =
"#version 150 \n"
" \n"
"precision highp float; \n"
"varying vec2 v_vTexCoord; \n"
"uniform sampler2D s_2dTexture; \n"
" \n"
"void main() { \n"
" gl_FragColor = \n"
" texture2D(s_2dTexture, v_vTexCoord); \n"
"} \n";
}
~TextureShader() override = default;
void Link() override
{
Shader::Link();
m_PositionAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vPosition");
m_TextureCoordinateAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vTexCoord");
m_SamplerHandle = glGetUniformLocation(m_ProgramId, "s_2dTexture");
}
void Setup(const Geometry& geometry) override
{
Shader::Setup(geometry);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_Texture.GetId());
glUniform1i(m_SamplerHandle, 0);
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);
glVertexAttribPointer(
m_PositionAttributeHandle,
geometry.GetNumVertexPositionElements(),
GL_FLOAT,
GL_FALSE,
geometry.GetVertexStride(),
geometry.GetVertices());
glEnableVertexAttribArray(m_PositionAttributeHandle);
glVertexAttribPointer(
m_TextureCoordinateAttributeHandle,
geometry.GetNumTexCoordElements(),
GL_FLOAT,
GL_FALSE,
geometry.GetVertexStride(),
geometry.GetTexCoords());
glEnableVertexAttribArray(m_TextureCoordinateAttributeHandle);
}
};
TextureShader class继承自Shader类。TextureShader class构造函数中的顶点着色器代码包含两个属性和一个变量。顶点的位置元素被直接传递给内置的gl_Position变量,该变量接收顶点的最终转换位置。a_vTexCoord属性被传递给v_vTexCoord变量。变量用于将插值数据从顶点着色器传输到片段着色器,因此顶点着色器和片段着色器都包含具有相同类型和名称的变量非常重要。OpenGL 在幕后工作,以确保来自顶点着色器的不同输出被传递到片段着色器中的相同输出。
片段着色器包含一个制服。统一更像着色器常量,因为它们由每个绘制调用的单个调用来设置,并且着色器的每个实例都接收相同的值。在这种情况下,片段着色器的每个实例都接收相同的采样器 ID,以从相同的纹理中检索数据。使用texture2D函数读取该数据,该函数采用统一的sampler2D和变化的v_vTexCoord。变化的纹理坐标已经被内插在一个多边形的表面上,所以该多边形是使用来自纹理数据的不同纹理元素来映射的。
在每次调用draw之前,TextureShader::Setup函数负责初始化采样器状态。使用glActiveTexture功能初始化你想要使用的纹理单元。使用glBindTexture将一个纹理绑定到这个纹理单元,传递给它的是 OpenGL 纹理的 ID。统一绑定有些不直观。glActiveTexture接收常量值GL_TEXTURE0作为值,而不是 0。这允许glActiveTexture调用将纹理与纹理图像单元绑定相关联,但是片段着色器不使用相同的值;相反,它使用纹理图像单元的索引。在这种情况下,GL_TEXTURE0可以在索引 0 处找到,因此值 0 被绑定到片段着色器中的m_SamplerHandle统一。
然后为绑定纹理初始化采样器参数。它们被设置为在两个方向上夹紧纹理。如果您想要使用纹理坐标的正常范围 0 到 1 之外的值,这将非常有用。在这些情况下,也可以设置纹理来包裹、重复或镜像。接下来的两个选项配置当纹理在屏幕上缩小或放大时的采样设置。当纹理被应用到一个比一对一映射的纹理占用更少屏幕空间的对象时,就会发生缩小。在屏幕上以 256 × 256 渲染 512 × 512 的纹理时,可能会出现这种情况。放大发生在相反的情况下,其中纹理被渲染到一个对象,该对象占用的屏幕空间比纹理提供的纹理像素多。线性映射使用最接近采样点的四个纹理元素来计算要应用于片段的颜色的平均值。这会以稍微模糊纹理为代价,使纹理看起来不那么块状。根据应用到纹理的缩小或放大程度,效果会更明显。
然后,TextureShader::Setup函数为顶点着色器的属性字段初始化数据流。使用来自几何对象的位置元素的数量以及来自该位置的步幅,将顶点位置元素绑定到m_PositionAttributeHandle位置。属性初始化后,通过调用glEnableVertexAttribArray来启用它。m_TextureCoordinateAttributeHandle属性使用相同的函数初始化,但使用不同的数据。从geometry对象中获取每个顶点的纹理元素数量,纹理坐标流也是如此。顶点数据和纹理数据的跨度保持不变,因为它们以结构数组格式打包到同一个流中。
清单 14-11 中的代码将所有这些结合在一起,并添加了一个main函数来展示如何初始化一个纹理和几何体,以将一个四边形渲染到应用了纹理图像的屏幕上。
清单 14-11 。纹理四边形程序
#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include <string>
#include <vector>
using namespace std;
class Geometry
{
public:
using Vertices = vector < float >;
using Indices = vector < unsigned short >;
private:
Vertices m_Vertices;
Indices m_Indices;
unsigned int m_NumVertexPositionElements{};
unsigned int m_NumTextureCoordElements{};
unsigned int m_VertexStride{};
public:
Geometry() = default;
~Geometry() = default;
void SetVertices(const Vertices& vertices)
{
m_Vertices = vertices;
}
Vertices::size_type GetNumVertices() const
{
return m_Vertices.size();
}
Vertices::const_pointer GetVertices() const
{
return m_Vertices.data();
}
void SetIndices(const Indices& indices)
{
m_Indices = indices;
}
Indices::size_type GetNumIndices() const
{
return m_Indices.size();
}
Indices::const_pointer GetIndices() const
{
return m_Indices.data();
}
Vertices::const_pointer GetTexCoords() const
{
return static_cast<Vertices::const_pointer>(&m_Vertices
[m_NumVertexPositionElements]);
}
void SetNumVertexPositionElements(unsigned int numVertexPositionElements)
{
m_NumVertexPositionElements = numVertexPositionElements;
}
unsigned int GetNumVertexPositionElements() const
{
return m_NumVertexPositionElements;
}
void SetNumTexCoordElements(unsigned int numTexCoordElements)
{
m_NumTextureCoordElements = numTexCoordElements;
}
unsigned int GetNumTexCoordElements() const
{
return m_NumTextureCoordElements;
}
void SetVertexStride(unsigned int vertexStride)
{
m_VertexStride = vertexStride;
}
unsigned int GetVertexStride() const
{
return m_VertexStride;
}
};
class TGAFile
{
private:
#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct TGAHeader
{
unsigned char m_IdSize{};
unsigned char m_ColorMapType{};
unsigned char m_ImageType{};
unsigned short m_PaletteStart{};
unsigned short m_PaletteLength{};
unsigned char m_PaletteBits{};
unsigned short m_XOrigin{};
unsigned short m_YOrigin{};
unsigned short m_Width{};
unsigned short m_Height{};
unsigned char m_BytesPerPixel{};
unsigned char m_Descriptor{};
}
#ifndef _MSC_VER
__attribute__ ((packed))
#endif // _MSC_VER
;
#ifdef _MSC_VER
#pragma pack(pop)
#endif
std::vector<char> m_FileData;
TGAHeader* m_pHeader{};
void* m_pImageData{};
public:
TGAFile(const std::string& filename)
{
std::ifstream fileStream{ filename, std::ios_base::binary };
if (fileStream.is_open())
{
fileStream.seekg(0, std::ios::end);
m_FileData.resize(static_cast<unsigned int>(fileStream.tellg()));
fileStream.seekg(0, std::ios::beg);
fileStream.read(m_FileData.data(), m_FileData.size());
fileStream.close();
m_pHeader = reinterpret_cast<TGAHeader*>(m_FileData.data());
m_pImageData = static_cast<void*>(m_FileData.data() + sizeof(TGAHeader));
}
}
unsigned short GetWidth() const
{
return m_pHeader->m_Width;
}
unsigned short GetHeight() const
{
return m_pHeader->m_Height;
}
unsigned char GetBytesPerPixel() const
{
return m_pHeader->m_BytesPerPixel;
}
unsigned int GetDataSize() const
{
return m_FileData.size() - sizeof(TGAHeader);
}
void* GetImageData() const
{
return m_pImageData;
}
};
class Texture
{
private:
unsigned int m_Width{};
unsigned int m_Height{};
unsigned int m_BytesPerPixel{};
unsigned int m_DataSize{};
GLuint m_Id{};
void* m_pImageData;
public:
Texture(const TGAFile& tgaFile)
: Texture(tgaFile.GetWidth(),
tgaFile.GetHeight(),
tgaFile.GetBytesPerPixel(),
tgaFile.GetDataSize(),
tgaFile.GetImageData())
{
}
Texture(unsigned int width,
unsigned int height,
unsigned int bytesPerPixel,
unsigned int dataSize,
void* pImageData)
: m_Width(width)
, m_Height(height)
, m_BytesPerPixel(bytesPerPixel)
, m_DataSize(dataSize)
, m_pImageData(pImageData)
{
}
~Texture() = default;
GLuint GetId() const
{
return m_Id;
}
void Init()
{
GLint packBits{ 4 };
GLint internalFormat{ GL_RGBA };
GLint format{ GL_BGRA };
glGenTextures(1, &m_Id);
glBindTexture(GL_TEXTURE_2D, m_Id);
glPixelStorei(GL_UNPACK_ALIGNMENT, packBits);
glTexImage2D(GL_TEXTURE_2D,
0,
internalFormat,
m_Width,
m_Height,
0,
format,
GL_UNSIGNED_BYTE,
m_pImageData);
}
};
class Shader
{
private:
void LoadShader(GLuint id, const std::string& shaderCode)
{
const unsigned int NUM_SHADERS{ 1 };
const char* pCode{ shaderCode.c_str() };
GLint length{ static_cast<GLint>(shaderCode.length()) };
glShaderSource(id, NUM_SHADERS, &pCode, &length);
glCompileShader(id);
glAttachShader(m_ProgramId, id);
}
protected:
GLuint m_VertexShaderId{ GL_INVALID_VALUE };
GLuint m_FragmentShaderId{ GL_INVALID_VALUE };
GLint m_ProgramId{ GL_INVALID_VALUE };
std::string m_VertexShaderCode;
std::string m_FragmentShaderCode;
public:
Shader() = default;
virtual ~Shader() = default;
virtual void Link()
{
m_ProgramId = glCreateProgram();
m_VertexShaderId = glCreateShader(GL_VERTEX_SHADER);
LoadShader(m_VertexShaderId, m_VertexShaderCode);
m_FragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
LoadShader(m_FragmentShaderId, m_FragmentShaderCode);
glLinkProgram(m_ProgramId);
}
virtual void Setup(const Geometry& geometry)
{
glUseProgram(m_ProgramId);
}
};
class TextureShader
: public Shader
{
private:
const Texture& m_Texture;
GLint m_PositionAttributeHandle;
GLint m_TextureCoordinateAttributeHandle;
GLint m_SamplerHandle;
public:
TextureShader(const Texture& texture)
: m_Texture(texture)
{
m_VertexShaderCode =
"attribute vec4 a_vPosition; \n"
"attribute vec2 a_vTexCoord; \n"
"varying vec2 v_vTexCoord; \n"
" \n"
"void main() { \n"
" gl_Position = a_vPosition; \n"
" v_vTexCoord = a_vTexCoord; \n"
"} \n";
m_FragmentShaderCode =
"#version 150 \n"
" \n"
"precision highp float; \n"
"varying vec2 v_vTexCoord; \n"
"uniform sampler2D s_2dTexture; \n"
" \n"
"void main() { \n"
" gl_FragColor = \n"
" texture2D(s_2dTexture, v_vTexCoord); \n"
"} \n";
}
~TextureShader() override = default;
void Link() override
{
Shader::Link();
m_PositionAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vPosition");
m_TextureCoordinateAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vTexCoord");
m_SamplerHandle = glGetUniformLocation(m_ProgramId, "s_2dTexture");
}
void Setup(const Geometry& geometry) override
{
Shader::Setup(geometry);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_Texture.GetId());
glUniform1i(m_SamplerHandle, 0);
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);
glVertexAttribPointer(
m_PositionAttributeHandle,
geometry.GetNumVertexPositionElements(),
GL_FLOAT,
GL_FALSE,
geometry.GetVertexStride(),
geometry.GetVertices());
glEnableVertexAttribArray(m_PositionAttributeHandle);
glVertexAttribPointer(
m_TextureCoordinateAttributeHandle,
geometry.GetNumTexCoordElements(),
GL_FLOAT,
GL_FALSE,
geometry.GetVertexStride(),
geometry.GetTexCoords());
glEnableVertexAttribArray(m_TextureCoordinateAttributeHandle);
}
};
int CALLBACK WinMain(
_In_ HINSTANCE hInstance,
_In_ HINSTANCE hPrevInstance,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow
)
{
GLFWwindow* window;
// Initialize the library
if (!glfwInit())
{
return -1;
}
// Create a windowed mode window and its OpenGL context
window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
// Make the window's context current
glfwMakeContextCurrent(window);
GLenum glewError{ glewInit() };
if (glewError != GLEW_OK)
{
return -1;
}
TGAFile myTextureFile("MyTexture.tga");
Texture myTexture(myTextureFile);
myTexture.Init();
TextureShader textureShader(myTexture);
textureShader.Link();
Geometry quad;
Geometry::Vertices vertices{
-0.5f, 0.5f, 0.0f,
0.0f, 1.0f,
0.5f, 0.5f, 0.0f,
1.0f, 1.0f,
-0.5f, -0.5f, 0.0f,
0.0f, 0.0f,
0.5f, -0.5f, 0.0f,
1.0f, 0.0f
};
Geometry::Indices indices{ 0, 2, 1, 2, 3, 1 };
quad.SetVertices(vertices);
quad.SetIndices(indices);
quad.SetNumVertexPositionElements(3);
quad.SetNumTexCoordElements(2);
quad.SetVertexStride(sizeof(float) * 5);
glClearColor(0.25f, 0.25f, 0.95f, 1.0f);
// Loop until the user closes the window
while (!glfwWindowShouldClose(window))
{
glClear(GL_COLOR_BUFFER_BIT);
textureShader.Setup(quad);
glDrawElements(GL_TRIANGLES,
quad.GetNumIndices(),
GL_UNSIGNED_SHORT,
quad.GetIndices());
// Swap front and back buffers
glfwSwapBuffers(window);
// Poll for and process events
glfwPollEvents();
}
glfwTerminate();
return 0;
}
清单 14-11 中程序的完整源代码展示了如何将这个配方中引入的所有类组合在一起,以渲染一个纹理四边形。初始化TGAFile类以加载MyTexture.tga文件。这被传递给类型为Texture的myTexture对象。调用Texture::Init函数来初始化 OpenGL texture对象。初始化的纹理又被传递给TextureShader类的一个实例,该实例创建、初始化并链接一个 OpenGL 着色器程序,该程序可用于渲染 2D 纹理几何图形。然后创建几何图形;指定的顶点包括每个顶点的三个位置元素和两个纹理坐标元素。OpenGL 使用四个顶点和六个索引来渲染由两个三角形组成的四边形。索引 1 和 2 处的顶点由两个三角形共享;您可以看到如何使用索引来减少网格所需的几何体定义。这里还有另一个优化优势:许多现代 CPU 缓存已经处理过的顶点的结果,因此您可以从缓存中读取重用的顶点数据,而不是让 GPU 重新处理它。
所有设置工作完成后,实际的渲染是微不足道的。有清除帧缓冲区、设置着色器、绘制元素、交换缓冲区和轮询操作系统事件的调用。图 14-2 显示了当一切都完成并正常工作时,这个程序的输出是什么样子。
图 14-2 。显示使用 OpenGL 渲染的纹理四边形的输出
14-4.从文件加载几何图形
问题
您希望能够从您团队中的艺术家创建的文件中加载网格数据。
解决办法
C++ 允许你编写可以加载许多不同文件格式的代码。这个食谱告诉你如何加载波前.obj文件。
它是如何工作的
.obj文件格式最初由 Wavefront Technologies 开发。它可以从许多 3D 建模程序中导出,并且是一种简单的基于文本的格式,使其成为学习如何导入 3D 数据的理想媒介。清单 14-12 中的OBJFile类 展示了如何从一个源文件加载一个.obj文件。
清单 14-12 。加载一个.obj文件
class OBJFile
{
public:
using Vertices = vector < float > ;
using TextureCoordinates = vector < float > ;
using Normals = vector < float > ;
using Indices = vector < unsigned short > ;
private:
Vertices m_VertexPositions;
TextureCoordinates m_TextureCoordinates;
Normals m_Normals;
Indices m_Indices;
public:
OBJFile(const std::string& filename)
{
std::ifstream fileStream{ filename, std::ios_base::in };
if (fileStream.is_open())
{
while (!fileStream.eof())
{
std::string line;
getline(fileStream, line);
stringstream lineStream{ line };
std::string firstSymbol;
lineStream >> firstSymbol;
if (firstSymbol == "v")
{
float vertexPosition{};
for (unsigned int i = 0; i < 3; ++i)
{
lineStream >> vertexPosition;
m_VertexPositions.emplace_back(vertexPosition);
}
}
else if (firstSymbol == "vt")
{
float textureCoordinate{};
for (unsigned int i = 0; i < 2; ++i)
{
lineStream >> textureCoordinate;
m_TextureCoordinates.emplace_back(textureCoordinate);
}
}
else if (firstSymbol == "vn")
{
float normal{};
for (unsigned int i = 0; i < 3; ++i)
{
lineStream >> normal;
m_Normals.emplace_back(normal);
}
}
else if (firstSymbol == "f")
{
char separator;
unsigned short index{};
for (unsigned int i = 0; i < 3; ++i)
{
for (unsigned int j = 0; j < 3; ++j)
{
lineStream >> index;
m_Indices.emplace_back(index);
if (j < 2)
{
lineStream >> separator;
}
}
}
}
}
}
}
const Vertices& GetVertices() const
{
return m_VertexPositions;
}
const TextureCoordinates& GetTextureCoordinates() const
{
return m_TextureCoordinates;
}
const Normals& GetNormals() const
{
return m_Normals;
}
const Indices& GetIndices() const
{
return m_Indices;
}
};
这段代码展示了如何从一个.obj文件中读取数据。.obj数据按行存储。代表顶点位置的线以字母 v 开始,包含三个浮点数,代表顶点的 x、y 和 z 位移。以 vt 开头的一行包含一个纹理坐标,两个浮点数代表纹理坐标的 u 和 v 分量。 vn 线代表顶点法线,包含顶点法线的 x、y 和 z 分量。您感兴趣的最后一种线条以 n 开头,代表三角形的索引。每个顶点在面中用三个数字表示:顶点位置列表的索引、纹理坐标的索引和顶点法线的索引。所有这些数据都被加载到类中的四个向量中;有从类中检索数据的访问器。清单 14-13 中的Geometry类有一个构造函数,它可以引用一个OBJFile对象并创建一个 OpenGL 可以渲染的网格。
清单 14-13 。Geometry类
class Geometry
{
public:
using Vertices = vector < float >;
using Indices = vector < unsigned short >;
private:
Vertices m_Vertices;
Indices m_Indices;
unsigned int m_NumVertexPositionElements{};
unsigned int m_NumTextureCoordElements{};
unsigned int m_VertexStride{};
public:
Geometry() = default;
Geometry(const OBJFile& objFile)
{
const OBJFile::Indices& objIndices{ objFile.GetIndices() };
const OBJFile::Vertices& objVertexPositions{ objFile.GetVertices() };
const OBJFile::TextureCoordinates& objTextureCoordinates{
objFile.GetTextureCoordinates() };
for (unsigned int i = 0; i < objIndices.size(); i += 3U)
{
m_Indices.emplace_back(i / 3);
const Indices::value_type index{ objIndices[i] - 1U };
const unsigned int vertexPositionIndex{ index * 3U };
m_Vertices.emplace_back(objVertexPositions[vertexPositionIndex]);
m_Vertices.emplace_back(objVertexPositions[vertexPositionIndex+1]);
m_Vertices.emplace_back(objVertexPositions[vertexPositionIndex+2]);
const OBJFile::TextureCoordinates::size_type texCoordObjIndex{
objIndices[i + 1] - 1U };
const unsigned int textureCoodsIndex{ texCoordObjIndex * 2U };
m_Vertices.emplace_back(objTextureCoordinates[textureCoodsIndex]);
m_Vertices.emplace_back(objTextureCoordinates[textureCoodsIndex+1]);
}
}
~Geometry() = default;
void SetVertices(const Vertices& vertices)
{
m_Vertices = vertices;
}
Vertices::size_type GetNumVertices() const
{
return m_Vertices.size();
}
Vertices::const_pointer GetVertices() const
{
return m_Vertices.data();
}
void SetIndices(const Indices& indices)
{
m_Indices = indices;
}
Indices::size_type GetNumIndices() const
{
return m_Indices.size();
}
Indices::const_pointer GetIndices() const
{
return m_Indices.data();
}
Vertices::const_pointer GetTexCoords() const
{
return static_cast<Vertices::const_pointer>(&m_Vertices[m_NumVertexPositionElements]);
}
void SetNumVertexPositionElements(unsigned int numVertexPositionElements)
{
m_NumVertexPositionElements = numVertexPositionElements;
}
unsigned int GetNumVertexPositionElements() const
{
return m_NumVertexPositionElements;
}
void SetNumTexCoordElements(unsigned int numTexCoordElements)
{
m_NumTextureCoordElements = numTexCoordElements;
}
unsigned int GetNumTexCoordElements() const
{
return m_NumTextureCoordElements;
}
void SetVertexStride(unsigned int vertexStride)
{
m_VertexStride = vertexStride;
}
unsigned int GetVertexStride() const
{
return m_VertexStride;
}
};
清单 14-13 包含了一个Geometry类的构造函数,它可以从一个OBJFile实例中为 OpenGL 构建几何图形。OBJFile::m_Indices向量包含每个 OpenGL 顶点的三个索引。这个配方的Geometry类只关心顶点位置索引和纹理坐标索引,但是for循环仍然被配置为每次迭代向前跳过三个索引。Geometry对象的顶点索引是obj索引除以 3;当前顶点由通过查找在for循环的每次迭代中获得的给定obj索引的obj顶点位置和纹理坐标获得的数据构成。.obj文件中的顶点索引和纹理坐标索引从 1 而不是 0 开始,所以从每个索引中减去 1 以得到正确的矢量索引。然后,顶点位置索引乘以 3,纹理坐标索引乘以 2,因为从原始.obj文件读取的每个顶点位置有三个元素,每个纹理坐标有两个元素。在循环结束时,您有一个Geometry对象,其中包含从文件中加载的顶点和纹理坐标数据。清单 14-14 中的代码展示了如何在程序中使用这些类来渲染一个使用 Blender 3D 建模包创建和导出的纹理球体。
注意这本书里的大部分食谱都是独立的,但是 OpenGL API 涵盖了很多执行看似简单的任务所必需的代码。清单 14-14 包含配方 14-3 中包含的
Texture、Shader和TextureShader类。
清单 14-14 。渲染一个有纹理的球体
#include <cassert>
#include <fstream>
#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include <memory>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
class OBJFile
{
public:
using Vertices = vector < float > ;
using TextureCoordinates = vector < float > ;
using Normals = vector < float > ;
using Indices = vector < unsigned short > ;
private:
Vertices m_VertexPositions;
TextureCoordinates m_TextureCoordinates;
Normals m_Normals;
Indices m_Indices;
public:
OBJFile(const std::string& filename)
{
std::ifstream fileStream{ filename, std::ios_base::in };
if (fileStream.is_open())
{
while (!fileStream.eof())
{
std::string line;
getline(fileStream, line);
stringstream lineStream{ line };
std::string firstSymbol;
lineStream >> firstSymbol;
if (firstSymbol == "v")
{
float vertexPosition{};
for (unsigned int i = 0; i < 3; ++i)
{
lineStream >> vertexPosition;
m_VertexPositions.emplace_back(vertexPosition);
}
}
else if (firstSymbol == "vt")
{
float textureCoordinate{};
for (unsigned int i = 0; i < 2; ++i)
{
lineStream >> textureCoordinate;
m_TextureCoordinates.emplace_back(textureCoordinate);
}
}
else if (firstSymbol == "vn")
{
float normal{};
for (unsigned int i = 0; i < 3; ++i)
{
lineStream >> normal;
m_Normals.emplace_back(normal);
}
}
else if (firstSymbol == "f")
{
char separator;
unsigned short index{};
for (unsigned int i = 0; i < 3; ++i)
{
for (unsigned int j = 0; j < 3; ++j)
{
lineStream >> index;
m_Indices.emplace_back(index);
if (j < 2)
{
lineStream >> separator;
}
}
}
}
}
}
}
const Vertices& GetVertices() const
{
return m_VertexPositions;
}
const TextureCoordinates& GetTextureCoordinates() const
{
return m_TextureCoordinates;
}
const Normals& GetNormals() const
{
return m_Normals;
}
const Indices& GetIndices() const
{
return m_Indices;
}
};
class Geometry
{
public:
using Vertices = vector < float >;
using Indices = vector < unsigned short >;
private:
Vertices m_Vertices;
Indices m_Indices;
unsigned int m_NumVertexPositionElements{};
unsigned int m_NumTextureCoordElements{};
unsigned int m_VertexStride{};
public:
Geometry() = default;
Geometry(const OBJFile& objFile)
{
const OBJFile::Indices& objIndices{ objFile.GetIndices() };
const OBJFile::Vertices& objVertexPositions{ objFile.GetVertices() };
const OBJFile::TextureCoordinates& objTextureCoordinates{
objFile.GetTextureCoordinates() };
for (unsigned int i = 0; i < objIndices.size(); i += 3U)
{
m_Indices.emplace_back(i / 3);
const Indices::value_type index{ objIndices[i] - 1U };
const unsigned int vertexPositionIndex{ index * 3U };
m_Vertices.emplace_back(objVertexPositions[vertexPositionIndex]);
m_Vertices.emplace_back(objVertexPositions[vertexPositionIndex+1]);
m_Vertices.emplace_back(objVertexPositions[vertexPositionIndex+2]);
const OBJFile::TextureCoordinates::size_type texCoordObjIndex{
objIndices[i + 1] - 1U };
const unsigned int textureCoodsIndex{ texCoordObjIndex * 2U };
m_Vertices.emplace_back(objTextureCoordinates[textureCoodsIndex]);
m_Vertices.emplace_back(objTextureCoordinates[textureCoodsIndex+1]);
}
}
~Geometry() = default;
void SetVertices(const Vertices& vertices)
{
m_Vertices = vertices;
}
Vertices::size_type GetNumVertices() const
{
return m_Vertices.size();
}
Vertices::const_pointer GetVertices() const
{
return m_Vertices.data();
}
void SetIndices(const Indices& indices)
{
m_Indices = indices;
}
Indices::size_type GetNumIndices() const
{
return m_Indices.size();
}
Indices::const_pointer GetIndices() const
{
return m_Indices.data();
}
Vertices::const_pointer GetTexCoords() const
{
return static_cast<Vertices::const_pointer>(&m_Vertices[m_NumVertexPositionElements]);
}
void SetNumVertexPositionElements(unsigned int numVertexPositionElements)
{
m_NumVertexPositionElements = numVertexPositionElements;
}
unsigned int GetNumVertexPositionElements() const
{
return m_NumVertexPositionElements;
}
void SetNumTexCoordElements(unsigned int numTexCoordElements)
{
m_NumTextureCoordElements = numTexCoordElements;
}
unsigned int GetNumTexCoordElements() const
{
return m_NumTextureCoordElements;
}
void SetVertexStride(unsigned int vertexStride)
{
m_VertexStride = vertexStride;
}
unsigned int GetVertexStride() const
{
return m_VertexStride;
}
};
class TGAFile
{
private:
#ifdef _MSC_VER
#pragma pack(push, 1)
#endif
struct TGAHeader
{
unsigned char m_IdSize{};
unsigned char m_ColorMapType{};
unsigned char m_ImageType{};
unsigned short m_PaletteStart{};
unsigned short m_PaletteLength{};
unsigned char m_PaletteBits{};
unsigned short m_XOrigin{};
unsigned short m_YOrigin{};
unsigned short m_Width{};
unsigned short m_Height{};
unsigned char m_BytesPerPixel{};
unsigned char m_Descriptor{};
}
#ifndef _MSC_VER
__attribute__ ((packed))
#endif // _MSC_VER
;
#ifdef _MSC_VER
#pragma pack(pop)
#endif
std::vector<char> m_FileData;
TGAHeader* m_pHeader{};
void* m_pImageData{};
public:
TGAFile(const std::string& filename)
{
std::ifstream fileStream{ filename, std::ios_base::binary };
if (fileStream.is_open())
{
fileStream.seekg(0, std::ios::end);
m_FileData.resize(static_cast<unsigned int>(fileStream.tellg()));
fileStream.seekg(0, std::ios::beg);
fileStream.read(m_FileData.data(), m_FileData.size());
fileStream.close();
m_pHeader = reinterpret_cast<TGAHeader*>(m_FileData.data());
m_pImageData = static_cast<void*>(m_FileData.data() + sizeof(TGAHeader));
}
}
unsigned short GetWidth() const
{
return m_pHeader->m_Width;
}
unsigned short GetHeight() const
{
return m_pHeader->m_Height;
}
unsigned char GetBytesPerPixel() const
{
return m_pHeader->m_BytesPerPixel;
}
unsigned int GetDataSize() const
{
return m_FileData.size() - sizeof(TGAHeader);
}
void* GetImageData() const
{
return m_pImageData;
}
};
class Texture
{
private:
unsigned int m_Width{};
unsigned int m_Height{};
unsigned int m_BytesPerPixel{};
unsigned int m_DataSize{};
GLuint m_Id{};
void* m_pImageData;
public:
Texture(const TGAFile& tgaFile)
: Texture(tgaFile.GetWidth(),
tgaFile.GetHeight(),
tgaFile.GetBytesPerPixel(),
tgaFile.GetDataSize(),
tgaFile.GetImageData())
{
}
Texture(unsigned int width,
unsigned int height,
unsigned int bytesPerPixel,
unsigned int dataSize,
void* pImageData)
: m_Width(width)
, m_Height(height)
, m_BytesPerPixel(bytesPerPixel)
, m_DataSize(dataSize)
, m_pImageData(pImageData)
{
}
~Texture() = default;
GLuint GetId() const
{
return m_Id;
}
void Init()
{
GLint packBits{ 4 };
GLint internalFormat{ GL_RGBA };
GLint format{ GL_BGRA };
glGenTextures(1, &m_Id);
glBindTexture(GL_TEXTURE_2D, m_Id);
glPixelStorei(GL_UNPACK_ALIGNMENT, packBits);
glTexImage2D(GL_TEXTURE_2D,
0,
internalFormat,
m_Width,
m_Height,
0,
format,
GL_UNSIGNED_BYTE,
m_pImageData);
}
};
class Shader
{
private:
void LoadShader(GLuint id, const std::string& shaderCode)
{
const unsigned int NUM_SHADERS{ 1 };
const char* pCode{ shaderCode.c_str() };
GLint length{ static_cast<GLint>(shaderCode.length()) };
glShaderSource(id, NUM_SHADERS, &pCode, &length);
glCompileShader(id);
glAttachShader(m_ProgramId, id);
}
protected:
GLuint m_VertexShaderId{ GL_INVALID_VALUE };
GLuint m_FragmentShaderId{ GL_INVALID_VALUE };
GLint m_ProgramId{ GL_INVALID_VALUE };
std::string m_VertexShaderCode;
std::string m_FragmentShaderCode;
public:
Shader() = default;
virtual ~Shader() = default;
virtual void Link()
{
m_ProgramId = glCreateProgram();
m_VertexShaderId = glCreateShader(GL_VERTEX_SHADER);
LoadShader(m_VertexShaderId, m_VertexShaderCode);
m_FragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER);
LoadShader(m_FragmentShaderId, m_FragmentShaderCode);
glLinkProgram(m_ProgramId);
}
virtual void Setup(const Geometry& geometry)
{
glUseProgram(m_ProgramId);
}
};
class TextureShader
: public Shader
{
private:
const Texture& m_Texture;
GLint m_PositionAttributeHandle;
GLint m_TextureCoordinateAttributeHandle;
GLint m_SamplerHandle;
public:
TextureShader(const Texture& texture)
: m_Texture(texture)
{
m_VertexShaderCode =
"attribute vec4 a_vPosition; \n"
"attribute vec2 a_vTexCoord; \n"
"varying vec2 v_vTexCoord; \n"
" \n"
"void main() { \n"
" gl_Position = a_vPosition; \n"
" v_vTexCoord = a_vTexCoord; \n"
"} \n";
m_FragmentShaderCode =
"#version 150 \n"
" \n"
"varying vec2 v_vTexCoord; \n"
"uniform sampler2D s_2dTexture; \n"
" \n"
"void main() { \n"
" gl_FragColor = \n"
" texture2D(s_2dTexture, v_vTexCoord); \n"
"} \n";
}
~TextureShader() override = default;
void Link() override
{
Shader::Link();
m_PositionAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vPosition");
m_TextureCoordinateAttributeHandle = glGetAttribLocation(m_ProgramId, "a_vTexCoord");
m_SamplerHandle = glGetUniformLocation(m_ProgramId, "s_2dTexture");
}
void Setup(const Geometry& geometry) override
{
Shader::Setup(geometry);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_Texture.GetId());
glUniform1i(m_SamplerHandle, 0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glVertexAttribPointer(
m_PositionAttributeHandle,
geometry.GetNumVertexPositionElements(),
GL_FLOAT,
GL_FALSE,
geometry.GetVertexStride(),
geometry.GetVertices());
glEnableVertexAttribArray(m_PositionAttributeHandle);
glVertexAttribPointer(
m_TextureCoordinateAttributeHandle,
geometry.GetNumTexCoordElements(),
GL_FLOAT,
GL_FALSE,
geometry.GetVertexStride(),
geometry.GetTexCoords());
glEnableVertexAttribArray(m_TextureCoordinateAttributeHandle);
}
};
int main(void)
{
GLFWwindow* window;
// Initialize the library
if (!glfwInit())
{
return -1;
}
glfwWindowHint(GLFW_RED_BITS, 8);
glfwWindowHint(GLFW_GREEN_BITS, 8);
glfwWindowHint(GLFW_BLUE_BITS, 8);
glfwWindowHint(GLFW_DEPTH_BITS, 8);
glfwWindowHint(GLFW_DOUBLEBUFFER, true);
// Create a windowed mode window and its OpenGL context
window = glfwCreateWindow(480, 480, "Hello World", NULL, NULL);
if (!window)
{
glfwTerminate();
return -1;
}
// Make the window's context current
glfwMakeContextCurrent(window);
GLenum glewError{ glewInit() };
if (glewError != GLEW_OK)
{
return -1;
}
TGAFile myTextureFile("earthmap.tga");
Texture myTexture(myTextureFile);
myTexture.Init();
TextureShader textureShader(myTexture);
textureShader.Link();
OBJFile objSphere("sphere.obj");
Geometry sphere(objSphere);
sphere.SetNumVertexPositionElements(3);
sphere.SetNumTexCoordElements(2);
sphere.SetVertexStride(sizeof(float) * 5);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);
glEnable(GL_DEPTH_TEST);
// Loop until the user closes the window
while (!glfwWindowShouldClose(window))
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
textureShader.Setup(sphere);
glDrawElements(GL_TRIANGLES,
sphere.GetNumIndices(),
GL_UNSIGNED_SHORT,
sphere.GetIndices());
// Swap front and back buffers
glfwSwapBuffers(window);
// Poll for and process events
glfwPollEvents();
}
glfwTerminate();
return 0;
}
清单 14-14 显示了如何使用本菜谱和菜谱 14-3 中的类加载和呈现一个.obj文件。在这个配方中,窗口的创建方式有一些不同。glfwWindowHint函数指定了您希望应用程序的帧缓冲区拥有的一些参数。这里最重要的是深度缓冲。深度缓冲区在现代 GPU 上工作,在渲染过程中在每个片段位置存储来自多边形的 z 组件的标准化设备坐标。然后,您可以使用深度测试来允许或禁止在渲染期间向帧缓冲区写入新颜色。这在渲染球体时非常有用,可以确保球体后部渲染的像素不会覆盖球体前部片段的颜色。
面剔除也被启用,以确保你只能看到每个多边形的正面。多边形可以有两条边:正面和背面。OpenGL 根据顶点的缠绕顺序确定多边形是正面还是背面。默认情况下,OpenGL 确定顶点以逆时针顺序指定的多边形面向前面,顶点以顺时针顺序指定的多边形面向后面。当对象旋转时,这种情况会发生变化,因此 OpenGL 可以在多边形不面向相机时尽早丢弃多边形。如果您愿意,您可以使用glFrontFace功能改变正面多边形的缠绕顺序。
从http://planetpixelemporium.com/earth.html获得的earthmap.tga纹理被加载以赋予球体行星地球的外观;球体本身是从名为sphere.obj的文件中加载的。您可以通过调用glEnable并传递GL_CULL_FACE常量来启用正面剔除;通过调用glCullFace指定要剔除的面。通过调用 glEnable 并传递GL_DEPTH_TEST来启用深度测试;并且传递glClear调用GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT以确保颜色缓冲器和深度缓冲器在每个渲染帧的开始都被清除。
编译并运行随本书附带的网站数据一起提供的代码,产生一个程序来渲染地球,如图图 14-3 所示。
图 14-3 。清单 14-14 中的代码生成的渲染过的地球