我们将重点关注以下两个主题:
earth.jpg(纹理图)
下面代码的init()函数中:
(x0,y0,z0)、(s0,t0) 对应上图中的位置 0;
(x1,y1,z1)、(s1,t1) 对应上图中的位置 1;
(x2,y2,z2)、(s2,t2) 对应上图中的位置 2;
(x3,y3,z3)、(s3,t3) 对应上图中的位置 3。
...
class Sphere {
private:
int numVertices;
std::vector<glm::vec3> vertices;
std::vector<glm::vec2> texCoords;
void init(float, float);
float toRadians(float degrees);
public:
Sphere();
Sphere(float R, float prec);
int getNumVertices();
std::vector<glm::vec3> getVertices();
std::vector<glm::vec2> getTexCoords();
};
...
Sphere::Sphere() {
init(1.0, 10.0);
}
Sphere::Sphere(float R, float angleSpan) {
init(R, angleSpan);
}
float Sphere::toRadians(float degrees) {
return (degrees * 2.0f * 3.14159f) / 360.0f;
}
int Sphere::getNumVertices() { return numVertices; }
std::vector<glm::vec3> Sphere::getVertices() { return vertices; }
std::vector<glm::vec2> Sphere::getTexCoords() { return texCoords; }
/*
* R:球半径,angleSpan:将球进行横向和纵向单位切分的角度
*/
void Sphere::init(float R, float angleSpan) {
for (float vAngle = -90; vAngle < 90; vAngle += angleSpan) { // 垂直方向:每angleSpan度一份
for (float hAngle = 0; hAngle < 360; hAngle += angleSpan) { // 水平方向:每angleSpan度一份
float vAngleRange = 90 + vAngle;
// hAngle∈[0°,360°),对应纹理图水平方向:左->右∈[0,1)
// vAngleRange∈[0°,180°),对应纹理图垂直方向:底->顶∈[0,1)
float x0 = (float)(R * cos(toRadians(vAngle)) * cos(toRadians(hAngle)));
float y0 = (float)(R * cos(toRadians(vAngle)) * sin(toRadians(hAngle)));
float z0 = (float)(R * sin(toRadians(vAngle)));
float s0 = hAngle / 360;
float t0 = vAngleRange / 180;
float x1 = (float)(R * cos(toRadians(vAngle)) * cos(toRadians(hAngle + angleSpan)));
float y1 = (float)(R * cos(toRadians(vAngle)) * sin(toRadians(hAngle + angleSpan)));
float z1 = (float)(R * sin(toRadians(vAngle)));
float s1 = (hAngle + angleSpan) / 360;
float t1 = vAngleRange / 180;
float x2 = (float)(R * cos(toRadians(vAngle + angleSpan)) * cos(toRadians(hAngle + angleSpan)));
float y2 = (float)(R * cos(toRadians(vAngle + angleSpan)) * sin(toRadians(hAngle + angleSpan)));
float z2 = (float)(R * sin(toRadians(vAngle + angleSpan)));
float s2 = (hAngle + angleSpan) / 360;
float t2 = (vAngleRange + angleSpan) / 180;
float x3 = (float)(R * cos(toRadians(vAngle + angleSpan)) * cos(toRadians(hAngle)));
float y3 = (float)(R * cos(toRadians(vAngle + angleSpan)) * sin(toRadians(hAngle)));
float z3 = (float)(R * sin(toRadians(vAngle + angleSpan)));
float s3 = hAngle / 360;
float t3 = (vAngleRange + angleSpan) / 180;
// 构建第一个三角形及相应纹理坐标
vertices.push_back(glm::vec3(x1, y1, z1));
vertices.push_back(glm::vec3(x3, y3, z3));
vertices.push_back(glm::vec3(x0, y0, z0));
texCoords.push_back(glm::vec2(s1, t1));
texCoords.push_back(glm::vec2(s3, t3));
texCoords.push_back(glm::vec2(s0, t0));
// 构建第二个三角形及相应纹理坐标
vertices.push_back(glm::vec3(x1, y1, z1));
vertices.push_back(glm::vec3(x2, y2, z2));
vertices.push_back(glm::vec3(x3, y3, z3));
texCoords.push_back(glm::vec2(s1, t1));
texCoords.push_back(glm::vec2(s2, t2));
texCoords.push_back(glm::vec2(s3, t3));
}
}
numVertices = vertices.size();
}
...
#define numVAOs 1
#define numVBOs 2
float cameraX, cameraY, cameraZ;
float sphLocX, sphLocY, sphLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
GLuint earthTexture;
// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat, rMat;
Sphere mySphere = Sphere();
void setupVertices(void) {
std::vector<glm::vec3> vert = mySphere.getVertices();
std::vector<glm::vec2> tex = mySphere.getTexCoords();
std::vector<float> vertValues;
std::vector<float> texValues;
for (int i = 0; i < mySphere.getNumVertices(); i++) {
vertValues.push_back(vert[i].x);
vertValues.push_back(vert[i].y);
vertValues.push_back(vert[i].z);
texValues.push_back(tex[i].s);
texValues.push_back(tex[i].t);
}
glGenVertexArrays(1, vao);
glBindVertexArray(vao[0]);
glGenBuffers(numVBOs, vbo);
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
// 注意:sizeof(vector)返回的不是数据块大小(对于数组才是的)。不过可通过vector起始位置指针访问连续向量空间。
// 前提是vector中存储的是标量类型的数值,所以对于上面 vert ,是无法完成功能的,所以要转换为 vertValues。
glBufferData(GL_ARRAY_BUFFER, vertValues.size() * sizeof(float), &vertValues[0], GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glBufferData(GL_ARRAY_BUFFER, texValues.size() * sizeof(float), &texValues[0], GL_STATIC_DRAW);
}
void init(GLFWwindow* window) {
...
cameraX = 0.0f; cameraY = 0.0f; cameraZ = 2.0f;
sphLocX = 0.0f; sphLocY = 0.0f; sphLocZ = -1.0f;
earthTexture = Utils::loadTexture("earth.jpg");
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(renderingProgram);
mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(sphLocX, sphLocY, sphLocZ));
mvMat = vMat * mMat;
glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));
glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(1);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, earthTexture);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glDrawArrays(GL_TRIANGLES, 0, mySphere.getNumVertices());
}
#version 430
layout (location = 0) in vec3 pos;
layout (location = 1) in vec2 texCoord;
out vec2 tc;
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
void main() {
gl_Position = proj_matrix * mv_matrix * vec4(pos, 1.0);
tc = texCoord;
}
#version 430
in vec2 tc;
out vec4 color;
layout (binding = 0) uniform sampler2D samp;
void main() {
color = texture(samp, tc);
}
在 C++/OpenGL 中构建 Torus(环面) 类可以用与 Sphere 类几乎完全相同的方式完成。但是,我们有机会利用OpenGL 对顶点索引的支持来利用我们在构建环面时创建的索引(我们也可以为球体做到这一点,但我们没有这样做)。对于具有数千个顶点的超大型模型,使用OpenGL 索引可以提高性能。索引缓冲对象(Element Buffer Object, EBO)
相当于OpenGL中的顶点数组的概念,是为了解决同一个顶点多次重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。我们使用传统的数组绘制(array drawing)方式绘制一个立方体时,可能每个面使用6个顶点(绘制两个三角形),其中2个顶点是共享的,重复了,那么一共需要制定36个顶点;如果进行压缩的话,实际上只需要8个顶点数据。为了减少这些不必要的数据,我们需要使用索引绘制(indexed drawing)
。
使用 OpenGL 索引时, 我们还需要将索引本身加载到 VBO 中。 我们生成一个额外的 VBO 用于保存索引。 由于每个索引值只是一个整型引用, 我们首先将索引数组复制到整型的 C++ vector
std::vector<int> ind = myTorus.getIndices(); // 环面索引的读取函数返回整型向量类型的索引
...
// vbo[0]:顶点坐标缓冲区;vbo[1]:纹理坐标缓冲区;vbo[2]:顶点索引值缓冲区
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[2]);// vbo[2]是新增的 VBO
glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size()*4, &ind[0], GL_STATIC_DRAW);
在 display()中,我们将 glDrawArrays()调用替换为 glDrawElements()
调用,它会告诉 OpenGL 利用索引 VBO 来查找要绘制的顶点。我们还需要使用 glBindBuffer()启用包含索引的 VBO,指定哪个 VBO 包含索引并且为 GL_ELEMENT_ARRAY_BUFFER 类型。代码如下:
umTorusIndices = myTorus.getNumIndices();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);
glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
OpenGL 能够识别 GL_ELEMENT_ARRAY_BUFFER 的存在并利用它来访问顶点属性。
通过索引可以在顶点数组中不包含重复的顶点数据,如画一个正方形:
vertices = {-1, 1, 0, //【0】
1, 1, 0, //【1】
1,-1, 0, //【2】
-1,-1, 0, //【3】
-1, 1, 0, //【0】
1,-1, 0} //【2】
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
OBJ 文件中的行,以字符标记开头,表示该行上的数据类型。一些常见的标签包括:
还有其他标签可以用来存储对象名称、使用的材质、曲线、阴影和许多其他细节。我们这里只讨论上面列出的 4 个标签,这些标签足以导入各种复杂模型。
蓝色v 开头的是所有三角形顶点坐标(x, y, z);
红色vt 开头的是所有顶点的纹理坐标(纹理坐标列表比顶点列表长的原因是一些顶点参与多个三角形,并且在这些情况下可能使用不同的纹理坐标);
绿色vn 开头的是顶点法向量(该列表通常也比顶点列表长,尽管在该示例中不是这样,同样是因为一些顶点参与多个三角形,并且在那些情况下可能使用不同的法向量);
紫色f 开头的是面。
[面]格式表示的含义:(请注意OBJ 索引从 1 开始)
格式:f 顶点1/纹理1/法向量1 顶点2/纹理2/法向量2 顶点3/纹理3/法向量3,
其中:顶点1(纹理1,法向量1) — 顶点2(纹理2,法向量2) — 顶点3(纹理3,法向量3) 构成一个三角形。
例如,第三个面是:f 2/7/3 5/8/3 3/9/3
这表明顶点列表(“v”开头)中的第2、第 5 和 第3个顶点组成了一个三角形。
相应的纹理坐标是纹理坐标列表(“vt”开头)中的第7、第8 和 第9项。
所有 3 个顶点都具有相同的法向量,也就是法向量列表(“vn”开头)中的第 3 项。
OBJ 格式的模型并不要求具有法向量, 甚至纹理坐标。
如果模型没有纹理坐标或法向量,则面的数值将仅指定顶点索引:f 2 5 3
如果模型具有纹理坐标,但不具有法向量,则格式为:f 2/7 5/8 3/9
如果模型具有法向量但没有纹理坐标,则格式为:f 2//3 5//3 3//3
void ModelImporter::parseOBJ(const char *filePath) {
float x, y, z;
string content;
ifstream fileStream(filePath, ios::in);
string line = "";
while (!fileStream.eof()) {
getline(fileStream, line);
if (line.compare(0, 2, "v ") == 0) {
stringstream ss(line.erase(0, 1));
ss >> x; ss >> y; ss >> z;
vertVals.push_back(x);
vertVals.push_back(y);
vertVals.push_back(z);
}
if (line.compare(0, 2, "vt") == 0) {
stringstream ss(line.erase(0, 2));
ss >> x; ss >> y;
stVals.push_back(x);
stVals.push_back(y);
}
if (line.compare(0, 2, "vn") == 0) {
stringstream ss(line.erase(0, 2));
ss >> x; ss >> y; ss >> z;
normVals.push_back(x);
normVals.push_back(y);
normVals.push_back(z);
}
if (line.compare(0, 2, "f ") == 0) {
string oneCorner, v, t, n;
stringstream ss(line.erase(0, 2));
for (int i = 0; i < 3; i++) {
getline(ss, oneCorner, ' ');
stringstream oneCornerSS(oneCorner);
getline(oneCornerSS, v, '/');
getline(oneCornerSS, t, '/');
getline(oneCornerSS, n, '/');
int vertRef = (stoi(v) - 1) * 3;
int tcRef = (stoi(t) - 1) * 2;
int normRef = (stoi(n) - 1) * 3;
triangleVerts.push_back(vertVals[vertRef]);
triangleVerts.push_back(vertVals[vertRef + 1]);
triangleVerts.push_back(vertVals[vertRef + 2]);
textureCoords.push_back(stVals[tcRef]);
textureCoords.push_back(stVals[tcRef + 1]);
normals.push_back(normVals[normRef]);
normals.push_back(normVals[normRef + 1]);
normals.push_back(normVals[normRef + 2]);
}
}
}
}
章中介绍的 OBJ 导入器的功能是很有限的,并且只能处理 OBJ 格式支持的一部分功能。它虽然足以满足我们的需求,但会在某些 OBJ 文件上失败。在这些情况下,有必要首先将模型加载到 Blender(或 Maya 等)工具中,然后将其重新导出为符合导入器限制的 OBJ 文件。