✺ch6——3D模型

目录

    • 程序构建模型——构建一个球体
    • OpenGL中的索引
    • 加载外部构建的模型

我们将重点关注以下两个主题:

  • 通过程序来构建模型;
  • 加载外部创建的模型。

程序构建模型——构建一个球体

earth.jpg(纹理图)
✺ch6——3D模型_第1张图片
✺ch6——3D模型_第2张图片
下面代码的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);
}

OpenGL中的索引

在 C++/OpenGL 中构建 Torus(环面) 类可以用与 Sphere 类几乎完全相同的方式完成。但是,我们有机会利用OpenGL 对顶点索引的支持来利用我们在构建环面时创建的索引(我们也可以为球体做到这一点,但我们没有这样做)。对于具有数千个顶点的超大型模型,使用OpenGL 索引可以提高性能。索引缓冲对象(Element Buffer Object, EBO)相当于OpenGL中的顶点数组的概念,是为了解决同一个顶点多次重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。我们使用传统的数组绘制(array drawing)方式绘制一个立方体时,可能每个面使用6个顶点(绘制两个三角形),其中2个顶点是共享的,重复了,那么一共需要制定36个顶点;如果进行压缩的话,实际上只需要8个顶点数据。为了减少这些不必要的数据,我们需要使用索引绘制(indexed drawing)

使用 OpenGL 索引时, 我们还需要将索引本身加载到 VBO 中。 我们生成一个额外的 VBO 用于保存索引。 由于每个索引值只是一个整型引用, 我们首先将索引数组复制到整型的 C++ vector中,然后使用 glBufferData() 将vector加载到新增的 VBO 中,指定 VBO 的类型为GL_ELEMENT_ARRAY_BUFFER,这就是告诉 OpenGL 这个 VBO 包含的是索引。

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 文件中的行,以字符标记开头,表示该行上的数据类型。一些常见的标签包括:

  • v-顶点坐标;
  • vt-纹理坐标;
  • vn-顶点法向量;
  • f-面(通常是三角形中的顶点)。

还有其他标签可以用来存储对象名称、使用的材质、曲线、阴影和许多其他细节。我们这里只讨论上面列出的 4 个标签,这些标签足以导入各种复杂模型。
✺ch6——3D模型_第3张图片
蓝色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 文件。

你可能感兴趣的:(#,第二版》,c++,OpenGL)