计算机图形学与opengl C++版 学习笔记 第6章 3D模型

到目前为止,我们只处理了非常简单的3D对象,例如立方体和金字塔。这些对象非常简单,我们能够在源代码中明确列出所有顶点信息,并将其直接放入缓冲区。

然而,大多数有趣的3D场景包括的对象过于复杂,使得我们无法像之前那样继续手工构建它们。在本章中,我们将探索更复杂的对象模型,以及如何构建并将它们加载到场景中。(请参考隐式几何和显式几何的介绍

3D建模本身就是一个广阔的领域,我们在这里讲到的必然非常有限。我们将重点关注以下两个主题:

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

虽然这只涉及丰富的3D建模领域中非常浅层的部分,但它将使我们能够在场景中包含各种复杂和逼真的细节对象。

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

某些类型的对象(例如球体、圆锥体等)具有数学定义,这些定义有助于算法生成。例如,对于半径为R的圆,围绕其圆周的点的坐标可以被很好地定义(见图6.1)。

在这里插入图片描述

图6.1 构成圆周的点

我们可以系统地使用圆的几何知识来通过算法建立球体模型。策略如下。

  1. 在整个球体上,选择表示一系列圆形“水平切片”的精度。 见图6.2的左侧。
  2. 将每个圆形切片的圆周细分为若干个点。见图6.2的右侧。 更多的点和水平切片可以生成更精确、更平滑的球体模型。在我们的 模型中,每个切片将具有相同数量的点。

在这里插入图片描述

图6.2 构建圆形顶点
  1. 将顶点分组为三角形。一种方法是逐步遍历顶点,在每一步构建两个三角形。例如,当我们沿着图6.3中球体上5个彩色顶点这一 行移动时,对于这5个顶点中的每一个,我们构建了以相应颜色显示的两个三角形(见彩插,下面将更详细地描述这些步骤)。

在这里插入图片描述

图6.3 将顶点组合成三角形
  1. 根据纹理图像的性质选择纹理坐标。在球体的情况下,存在许多地形纹理图像,假设我们选择这种纹理图像,想象一下,让这个图像围绕球体“包裹”,我们可以根据图像中纹素的最终对应位置为每个顶点指定纹理坐标。
  2. 对于每个顶点,通常还希望生成法向量(Normal Vector)——垂直于模型表面的向量。我们将很快在第7章中将它们用于光照。

确定法向量可能很棘手,但是在球体的情况下,从球体中心指向顶点的向量恰好等于该顶点的法向量!图6.4说明了这个特点(球体的中心用“星形”表示)。
在这里插入图片描述

图6.4 球体顶点法向量

一些模型使用索引定义三角形。请注意,在图6.3中,每个顶点出现在多个三角形中,这将导致每个顶点被多次指定。我们不希望这样做,而是会存储每个顶点一次,然后为三角形的每个角指定索引,引用所需的顶点。我们需要存储每个顶点的位置、纹理坐标和法向量, 因此这么做可以为大型模型节省内存。

顶点存储在一维数组中,从最下面的水平切片中的顶点开始。使用索引时,关联的索引数组包括每个三角形角的条目。其内容是顶点数组中的整型引用(具体地说,是下标)。假设每个切片包含n个顶点,顶点数组以及相应索引数组的示例部分,如图6.5所示。
在这里插入图片描述

图6.5 顶点数组和相应的索引数组

然后,我们可以从球体底部开始,围绕每个水平切片以圆形方式遍历顶点。当我们访问每个顶点时,我们构建两个三角形,在其右上方形成一个方形区域,如图6.3所示。我们将整个处理过程组织成嵌套循环,如下所示。

在这里插入图片描述

例如,考虑图6.3中的“红色”顶点(图6.6中重复出现)。这个顶点位于图6.6所示的黄色三角形的左下方,按照我们刚刚描述的循环,它的索引序号是 i*n+j。其中i是当前正在处理的切片(外循环),j是当前正在该切片中处理的顶点(内循环),n是每个切片的顶点数。图6.6显示了这个顶点(红色)以及它的3个相关的相邻顶点
(见彩插),每个顶点都有公式显示它们的索引序号。

在这里插入图片描述

图6.6 第i个切片中的第j个顶点的索引序号(n = 每个切片的顶点数)

然后使用这4个顶点构建为此(红色)顶点生成的两个三角形(以黄色显示)。这两个三角形的索引表中的6个条目在图中以数字1~6的顺序表示。注意,条目3和6都指向相同的顶点,对于条目2和4也是如此。当我们到达以红色突出显示的顶点(即vertex[i*n+j ])时由此定义的两个三角形是由这6个顶点构成的——其中一个三角形的条目标记为1、2、3,引用的顶点包括vertex[i *n+j]、vertex[i *n+j +1]和vertex[(i+1) *n+j ];另一个三角形的条目标记为4、5、6,引用的顶点包括vertex[i *n+j +1]、vertex[(i+1) *n+j +1]和vertex[(i+1) *n+j ]。

程序6.1显示了我们的球体模型的实现,类名为Sphere。生成的球体的中心位于原点。这里还显示了使用Sphere的代码。请注意,每个顶点都存储在包含GLM类vec2和vec3实例的C++向量中(这与之前的示例不同,之前顶点存储在浮点数组中)。vec2和vec3包括了获得所需x、y和z分量浮点值的方法,然后就可以如前所述将它们放入浮点缓冲区。我们将这些值存储在可变长度C++向量中,因为长度取决于运行时指定的切片数。

请注意Sphere类中三角形索引的计算,如前面的图6.6所述。变量“prec(precision)”指的是“精度”,在这里它被用来确定球形切片的数量和每个切片中的顶点数量。因为纹理贴图完全包裹在球体周围,所以在纹理贴图的左右边缘相交的每个点处需要一个额外的重合顶点。因此,顶点的总数是(prec+1) * (prec+1)。由于每个顶点生成6个三角形索引,因此索引的总数是prec * prec * 6。

程序6.1 程序生成的球体

vertShader.glsl

#version 430

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 tex_coord;
out vec2 tc;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D s;

void main(void)
{
    
    	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	tc = tex_coord;
}

fragShader.glsl

#version 430

in vec2 tc;
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D s;

void main(void)
{
    
    	color = texture(s,tc);
}

球体类(Sphere.cpp)

#include <cmath>
#include <vector>
#include <iostream>
#include <glm\glm.hpp>
#include "Sphere.h"
using namespace std;

Sphere::Sphere() {
    
    
	init(48);
}

Sphere::Sphere(int prec) {
    
    
	init(prec);
}

float Sphere::toRadians(float degrees) {
    
     return (degrees * 2.0f * 3.14159f) / 360.0f; }//精度

void Sphere::init(int prec) {
    
    
	numVertices = (prec + 1) * (prec + 1);
	numIndices = prec * prec * 6;
	for (int i = 0; i < numVertices; i++) {
    
     vertices.push_back(glm::vec3()); }
	for (int i = 0; i < numVertices; i++) {
    
     texCoords.push_back(glm::vec2()); }
	for (int i = 0; i < numVertices; i++) {
    
     normals.push_back(glm::vec3()); }
	for (int i = 0; i < numVertices; i++) {
    
     tangents.push_back(glm::vec3()); }
	for (int i = 0; i < numIndices; i++) {
    
     indices.push_back(0); }

	// 计算三角形顶点
	for (int i = 0; i <= prec; i++) {
    
    
		for (int j = 0; j <= prec; j++) {
    
    
			float y = (float)cos(toRadians(180.0f - i * 180.0f / prec));
			float x = -(float)cos(toRadians(j*360.0f / prec))*(float)abs(cos(asin(y)));
			float z = (float)sin(toRadians(j*360.0f / (float)(prec)))*(float)abs(cos(asin(y)));
			vertices[i*(prec + 1) + j] = glm::vec3(x, y, z);
			texCoords[i*(prec + 1) + j] = glm::vec2(((float)j / prec), ((float)i / prec));
			normals[i*(prec + 1) + j] = glm::vec3(x, y, z);

			// 计算切向量
			if (((x == 0) && (y == 1) && (z == 0)) || ((x == 0) && (y == -1) && (z == 0))) {
    
    
				tangents[i*(prec + 1) + j] = glm::vec3(0.0f, 0.0f, -1.0f);
			}
			else {
    
    
				tangents[i*(prec + 1) + j] = glm::cross(glm::vec3(0.0f, 1.0f, 0.0f), glm::vec3(x, y, z));
			}
		}
	}
	// 计算三角形索引
	for (int i = 0; i<prec; i++) {
    
    
		for (int j = 0; j<prec; j++) {
    
    
			indices[6 * (i*prec + j) + 0] = i*(prec + 1) + j;
			indices[6 * (i*prec + j) + 1] = i*(prec + 1) + j + 1;
			indices[6 * (i*prec + j) + 2] = (i + 1)*(prec + 1) + j;
			indices[6 * (i*prec + j) + 3] = i*(prec + 1) + j + 1;
			indices[6 * (i*prec + j) + 4] = (i + 1)*(prec + 1) + j + 1;
			indices[6 * (i*prec + j) + 5] = (i + 1)*(prec + 1) + j;
		}
	}
}

int Sphere::getNumVertices() {
    
     return numVertices; }
int Sphere::getNumIndices() {
    
     return numIndices; }
std::vector<int> Sphere::getIndices() {
    
     return indices; }
std::vector<glm::vec3> Sphere::getVertices() {
    
     return vertices; }
std::vector<glm::vec2> Sphere::getTexCoords() {
    
     return texCoords; }
std::vector<glm::vec3> Sphere::getNormals() {
    
     return normals; }
std::vector<glm::vec3> Sphere::getTangents() {
    
     return tangents; }

球体类(Sphere.h)

#include <cmath>
#include <vector>
#include <glm\glm.hpp>
class Sphere
{
    
    
private:
	int numVertices;
	int numIndices;
	std::vector<int> indices;
	std::vector<glm::vec3> vertices;
	std::vector<glm::vec2> texCoords;
	std::vector<glm::vec3> normals;
	std::vector<glm::vec3> tangents;
	void init(int);
	float toRadians(float degrees);

public:
	Sphere();
	Sphere(int prec);
	int getNumVertices();
	int getNumIndices();
	std::vector<int> getIndices();
	std::vector<glm::vec3> getVertices();
	std::vector<glm::vec2> getTexCoords();
	std::vector<glm::vec3> getNormals();
	std::vector<glm::vec3> getTangents();
};

main.cpp

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Sphere.h"
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 3

float cameraX, cameraY, cameraZ;
float sphLocX, sphLocY, sphLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
GLuint earthTexture;
float rotAmt = 0.0f;

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;

Sphere mySphere = Sphere(48); //球形切片的数量和每个切片中的顶点数量 顶点的总数是(48+1) * (48+1) 索引的总数是prec * prec * 6

void setupVertices(void) {
    
    
	std::vector<int> ind = mySphere.getIndices();
	std::vector<glm::vec3> vert = mySphere.getVertices();
	std::vector<glm::vec2> tex = mySphere.getTexCoords();
	std::vector<glm::vec3> norm = mySphere.getNormals();

	std::vector<float> pvalues;
	std::vector<float> tvalues;
	std::vector<float> nvalues;

	int numIndices = mySphere.getNumIndices();
	for (int i = 0; i < numIndices; i++) {
    
    
		pvalues.push_back((vert[ind[i]]).x);
		pvalues.push_back((vert[ind[i]]).y);
		pvalues.push_back((vert[ind[i]]).z);
		tvalues.push_back((tex[ind[i]]).s);
		tvalues.push_back((tex[ind[i]]).t);
		nvalues.push_back((norm[ind[i]]).x);
		nvalues.push_back((norm[ind[i]]).y);
		nvalues.push_back((norm[ind[i]]).z);
	}

	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, pvalues.size()*4, &pvalues[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
	glBufferData(GL_ARRAY_BUFFER, tvalues.size()*4, &tvalues[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
	glBufferData(GL_ARRAY_BUFFER, nvalues.size()*4, &nvalues[0], GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 2.0f;
	sphLocX = 0.0f; sphLocY = 0.0f; sphLocZ = -1.0f;

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	setupVertices();
	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.getNumIndices());
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter6 - program1", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

使用Sphere类时,每个顶点位置和法向量需要3个值,但每个纹理坐标只需要两个值。这反映在Sphere.h文件中显示的向量(vertices、texCoords和normals)的声明中,稍后数据从这些向量中加载到缓冲区中。

值得注意的是,虽然在构建球体的过程中使用了索引,但存储在 VBO中的最终球体顶点数据不使用索引。相反,当setupVertices()循环遍历球体索引时,它会在VBO中为每个索引条目生成单独的(通常是冗余的)顶点条目。OpenGL确实有一种索引顶点数据的机制;为简单起见,我们在此示例中没有使用它,但我们将在下一个示例中使用 OpenGL的索引。

从几何形状到现实世界的物体,使用程序的方式可以创建许多其他的模型。其中最著名的一个是“犹他茶壶”[CH16],在1975年由马丁· 纽厄尔(Martin Newell)开发,使用各种贝塞尔曲线和曲面。 OpenGL Utility Toolkit(或“GLUT”)[GL16]甚至包括了绘制茶壶的 程序(见图6.7)。我们在本书中没有涉及GLUT,但贝塞尔曲面将在第11章中介绍。

在这里插入图片描述

图6.7 OpenGL GLUT茶壶

6.2 OpenGL索引——构建一个环面

6.2.1 环面

用于产生环面的算法可以在各种网站上找到。Paul Baker逐步描述了定义圆形切片,然后围绕圆圈旋转切片以形成环面的方法[PP07]。 图6.8显示了侧面和上面的两种视图。
在这里插入图片描述

图6.8 构建一个环面

生成环面顶点位置的方式与构建球体的方式有很大不同。对于环面,算法将一个顶点定位到原点的右侧,然后在XY平面上的圆中让这个顶点围绕Z轴旋转,以形成“环”。然后,将这个环“向外”移动“内半径”那么长的距离。在构建这些顶点时,为每个顶点计算纹理坐标和法向量。还会额外为每个顶点生成与环面表面相切的向量(称为切向量)。

围绕Y轴旋转最初的这个环,形成用来构成环面的其他环的顶点。 轴旋转最初的环的切向量和法向量来计算每个结果顶点的切向量和法向量。在顶点创建之后,逐个环地遍历所有顶点,并且对于每个顶点生成两个三角形。两个三角形的六个索引表条目的生成方式和之前的球体类似。

我们为剩余的环选择纹理坐标的策略,是将它们排列成使得纹理图像的S轴环绕环面的水平周边的一半,然后再对另一半重复。当我们绕Y轴旋转生成环时,我们指定一个从1开始并增加到指定精度的变量环(再次称“prec”)。然后我们将S纹理坐标值设置为ring*2.0/prec,使S的取值范围介于0.0和2.0之间,然后每当纹理坐标大于1.0时减去1.0。这种方法的动机是避免纹理图像在水平方向上过度“拉伸”。反之,如果我们确实希望纹理完全围绕环面拉伸,我们只须从纹理坐标计算中删除“*2.0”乘数即可。

在C++/OpenGL中构建Torus类可以用与Sphere类几乎完全相同的方式完成。但是,我们有机会利用OpenGL对顶点索引的支持来利用我们在构建环面时创建的索引(我们也可以为球体做到这一点,但我们没有这样做)。对于具有数千个顶点的超大型模型,使用OpenGL索引可以提高性能,因此我们将描述如何执行此操作。

6.2.2 OpenGL中的索引

在我们的球体和环面模型中,我们生成一个引用顶点数组的整型索引数组。在球体的情况下,我们使用索引列表来构建一组完整的单个顶点,并将它们加载到VBO中,就像我们在前面章节的示例中所做的那样。实例化环面并将其顶点、法向量等加载到缓冲区中可以采用与 程序6.1中类似的方式完成,但我们将使用OpenGL的索引。

使用OpenGL索引时,我们还需要将索引本身加载到VBO中。我们生成一个额外的VBO用于保存索引。由于每个索引值只是一个整型引用, 我们首先将索引数组复制到整型的C++向量中,然后使用glBufferData()将向量加载到新增的VBO中,指定VBO的类型为 GL_ELEMENT_ARRAY_BUFFER(这会告诉OpenGL这个VBO包含索引)。执行此操作的代码可以添加到setupVertices()
在这里插入图片描述

在display()方法中,我们将glDrawArrays()调用替换为 glDrawElements()调用,它告诉OpenGL利用索引VBO来查找要绘制的顶点。我们还使用glBindBuffer()启用包含索引的VBO,指定哪个VBO包含索引并且是GL_ELEMENT_ARRAY_BUFFER类型。代码如下:

在这里插入图片描述

有趣的是,即使我们在C++/OpenGL应用程序中进行了更改,实现了索引,用于绘制球体的着色器对于环面来说仍然可以继续工作,不需要修改。OpenGL能够识别GL_ELEMENT_ARRAY_BUFFER的存在并利用它来访问顶点属性。

程序6.2显示了一个基于Baker实现的名为Torus的类.“内”和 “外”变量指的是图6.9中相应的内半径和外半径。prec变量具有与球体类似的作用,对顶点数量和索引数量进行类似的计算。相比之下, 确定法向量比使用球体复杂得多。我们使用了Baker描述中给出的策略,其中计算了两个切向量(Baker称为sTangent和tTangent,尽管通常称为“切向量(tangent)”和“副切向量(bitangent)”),它们的叉乘积形成法向量。

在本书的其余部分中,我们将在许多示例中使用此环面类(以及前面描述的球体类)。

程序6.2 程序生成的环面
vertShader.glsl

#version 430

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 tex_coord;
out vec2 tc;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D s;

void main(void)
{
    
    	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	tc = tex_coord;
}

fragShader.glsl

#version 430

in vec2 tc;
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D s;

void main(void)
{
    
    
	color = texture(s,tc);
}

Torus.h

#include <cmath>
#include <vector>
#include <glm\glm.hpp>
class Torus
{
    
    
private:
	int numVertices;
	int numIndices;
	int prec;
	float inner;
	float outer;
	std::vector<int> indices;
	std::vector<glm::vec3> vertices;
	std::vector<glm::vec2> texCoords;
	std::vector<glm::vec3> normals;
	std::vector<glm::vec3> sTangents;
	std::vector<glm::vec3> tTangents;
	void init();
	float toRadians(float degrees);

public:
	Torus();
	Torus(float inner, float outer, int prec);
	int getNumVertices();
	int getNumIndices();
	std::vector<int> getIndices();
	std::vector<glm::vec3> getVertices();
	std::vector<glm::vec2> getTexCoords();
	std::vector<glm::vec3> getNormals();
	std::vector<glm::vec3> getStangents();
	std::vector<glm::vec3> getTtangents();
};

Torus.cpp

#include <cmath>
#include <vector>
#include <iostream>
#include <glm\gtc\matrix_transform.hpp>
#include "Torus.h"
using namespace std;

Torus::Torus() {
    
    
	prec = 48;//精度
	inner = 0.5f;//内半径
	outer = 0.2f;//外半径
	init();
}

Torus::Torus(float in, float out, int precIn) {
    
    
	prec = precIn;
	inner = in;
	outer = out;
	init();
}

float Torus::toRadians(float degrees) {
    
     return (degrees * 2.0f * 3.14159f) / 360.0f; }

void Torus::init() {
    
    
	numVertices = (prec + 1) * (prec + 1);
	numIndices = prec * prec * 6;
	for (int i = 0; i < numVertices; i++) {
    
     vertices.push_back(glm::vec3()); }
	for (int i = 0; i < numVertices; i++) {
    
     texCoords.push_back(glm::vec2()); }
	for (int i = 0; i < numVertices; i++) {
    
     normals.push_back(glm::vec3()); }
	for (int i = 0; i < numVertices; i++) {
    
     sTangents.push_back(glm::vec3()); }
	for (int i = 0; i < numVertices; i++) {
    
     tTangents.push_back(glm::vec3()); }
	for (int i = 0; i < numIndices; i++) {
    
     indices.push_back(0); }

	//计算第一个圆环
	for (int i = 0; i < prec + 1; i++) {
    
    
		float amt = toRadians(i*360.0f / prec);

		glm::mat4 rMat = glm::rotate(glm::mat4(1.0f), amt, glm::vec3(0.0f, 0.0f, 1.0f));
		glm::vec3 initPos(rMat * glm::vec4(outer, 0.0f, 0.0f, 1.0f));

		vertices[i] = glm::vec3(initPos + glm::vec3(inner, 0.0f, 0.0f));
		// 为环上的每个顶点计算纹理坐标
		texCoords[i] = glm::vec2(0.0f, ((float)i / (float)prec));

		rMat = glm::rotate(glm::mat4(1.0f), amt, glm::vec3(0.0f, 0.0f, 1.0f));
		//计算了两个切向量(Baker称为sTangent和tTangent,尽管通常称为“切向量(tangent)”和“副切向量(bitangent)”),它们的叉乘积形成法向量
		tTangents[i] = glm::vec3(rMat * glm::vec4(0.0f, -1.0f, 0.0f, 1.0f));// 计算切向量和法向量,第一个切向量是绕Z轴旋转的Y轴

		sTangents[i] = glm::vec3(glm::vec3(0.0f, 0.0f, -1.0f));// 第二个切向量是 -Z 轴
		normals[i] = glm::cross(tTangents[i], sTangents[i]);// 它们的叉乘积就是法向量
	}
	// 绕原点旋转点,形成环,然后将它们向外移动
	for (int ring = 1; ring < prec + 1; ring++) {
    
    
		for (int i = 0; i < prec + 1; i++) {
    
    
			// 绕Y轴旋转最初那个环的顶点坐标
			float amt = (float)toRadians((float)ring * 360.0f / (prec));

			glm::mat4 rMat = glm::rotate(glm::mat4(1.0f), amt, glm::vec3(0.0f, 1.0f, 0.0f));
			vertices[ring*(prec + 1) + i] = glm::vec3(rMat * glm::vec4(vertices[i], 1.0f));
			// 计算新环顶点的纹理坐标
			texCoords[ring*(prec + 1) + i] = glm::vec2((float)ring*2.0f / (float)prec, texCoords[i].t);
			if (texCoords[ring*(prec + 1) + i].s > 1.0) texCoords[ring*(prec+1)+i].s -= 1.0f;
			// 绕Y轴旋转切向量和副切向量
			rMat = glm::rotate(glm::mat4(1.0f), amt, glm::vec3(0.0f, 1.0f, 0.0f));
			sTangents[ring*(prec + 1) + i] = glm::vec3(rMat * glm::vec4(sTangents[i], 1.0f));
			// 绕Y轴旋转法向量
			rMat = glm::rotate(glm::mat4(1.0f), amt, glm::vec3(0.0f, 1.0f, 0.0f));
			tTangents[ring*(prec + 1) + i] = glm::vec3(rMat * glm::vec4(tTangents[i], 1.0f));
			
			rMat = glm::rotate(glm::mat4(1.0f), amt, glm::vec3(0.0f, 1.0f, 0.0f));
			normals[ring*(prec + 1) + i] = glm::vec3(rMat * glm::vec4(normals[i], 1.0f));
		}
	}
	// 按照逐个顶点的两个三角形,计算三角形索引
	for (int ring = 0; ring < prec; ring++) {
    
    
		for (int i = 0; i < prec; i++) {
    
    
			indices[((ring*prec + i) * 2) * 3 + 0] = ring*(prec + 1) + i;
			indices[((ring*prec + i) * 2) * 3 + 1] = (ring + 1)*(prec + 1) + i;
			indices[((ring*prec + i) * 2) * 3 + 2] = ring*(prec + 1) + i + 1;
			indices[((ring*prec + i) * 2 + 1) * 3 + 0] = ring*(prec + 1) + i + 1;
			indices[((ring*prec + i) * 2 + 1) * 3 + 1] = (ring + 1)*(prec + 1) + i;
			indices[((ring*prec + i) * 2 + 1) * 3 + 2] = (ring + 1)*(prec + 1) + i + 1;
		}
	}
}
// 环面索引和顶点的访问函数
int Torus::getNumVertices() {
    
     return numVertices; }
int Torus::getNumIndices() {
    
     return numIndices; }
std::vector<int> Torus::getIndices() {
    
     return indices; }
std::vector<glm::vec3> Torus::getVertices() {
    
     return vertices; }
std::vector<glm::vec2> Torus::getTexCoords() {
    
     return texCoords; }
std::vector<glm::vec3> Torus::getNormals() {
    
     return normals; }
std::vector<glm::vec3> Torus::getStangents() {
    
     return sTangents; }
std::vector<glm::vec3> Torus::getTtangents() {
    
     return tTangents; }

main.cpp

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Torus.h"
#include "Utils.h"
using namespace std;

float toRadians(float degrees) {
    
     return (degrees * 2.0f * 3.14159f) / 360.0f; }

#define numVAOs 1
#define numVBOs 4

float cameraX, cameraY, cameraZ;
float torLocX, torLocY, torLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
GLuint brickTexture;
float rotAmt = 0.0f;

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;

Torus myTorus(0.5f, 0.2f, 48);

void setupVertices(void) {
    
    
	std::vector<int> ind = myTorus.getIndices();//将索引数组复制到整型的C++向量
	std::vector<glm::vec3> vert = myTorus.getVertices();
	std::vector<glm::vec2> tex = myTorus.getTexCoords();
	std::vector<glm::vec3> norm = myTorus.getNormals();

	std::vector<float> pvalues;
	std::vector<float> tvalues;
	std::vector<float> nvalues;

	for (int i = 0; i < myTorus.getNumVertices(); i++) {
    
    
		pvalues.push_back(vert[i].x);
		pvalues.push_back(vert[i].y);
		pvalues.push_back(vert[i].z);
		tvalues.push_back(tex[i].s);
		tvalues.push_back(tex[i].t);
		nvalues.push_back(norm[i].x);
		nvalues.push_back(norm[i].y);
		nvalues.push_back(norm[i].z);
	}
	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);//这里为什么要*4

	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
	glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
	glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]); //使用OpenGL索引时,需要将索引本身加载到VBO中。生成一个额外的VBO用于保存索引
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, ind.size() * 4, &ind[0], GL_STATIC_DRAW);//将向量加载到新增的VBO 指定VBO的类型为 `GL_ELEMENT_ARRAY_BUFFER`(告诉OpenGL这个VBO包含索引)
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 2.0f;
	torLocX = 0.0f; torLocY = 0.0f; torLocZ = -0.5f;

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	setupVertices();
	brickTexture = Utils::loadTexture("brick1.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(torLocX, torLocY, torLocZ));
	//mMat *= glm::eulerAngleXYZ(toRadians(30.0f), 0.0f, 0.0f);
	mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));

	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, brickTexture);

	glEnable(GL_CULL_FACE);
	glFrontFace(GL_CCW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo[3]);//启用包含索引的VBO,指定哪个VBO包含索引并且是`GL_ELEMENT_ARRAY_BUFFER类型
	glDrawElements(GL_TRIANGLES, myTorus.getIndices().size(), GL_UNSIGNED_INT, 0);//将`glDrawArrays()`调用替换为 `glDrawElements()`调用 告诉OpenGL利用索引VBO来查找要绘制的顶点
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(800, 800, "Chapter6 - program2", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

请注意,在使用Torus类的代码中,setupVertices()中的循环现 在只存储一次与每个顶点关联的数据,而不是每个索引条目存储一次(如球体示例中的情况)。这种差异也反映在要输入VBO的数据的数组 声明大小中。另请注意,在环面示例中,不是在检索顶点数据时使用 索引值,而是直接将它们简单地加载到VBO #3中。由于此VBO被指定为 GL_ELEMENT_ARRAY_BUFFER,OpenGL知道该VBO包含顶点索引。

图6.9显示了实例化环面并使用砖纹理对其进行纹理化的结果。

在这里插入图片描述

图6.9 程序生成的环面

6.3 加载外部构建的模型

复杂的3D模型,例如在视频游戏或计算机生成的电影中的人物角色,通常使用建模工具生成。这种“DCC”(数字内容创建)工具使人们(例如艺术家)能够在3D空间中构建任意形状并自动生成顶点、纹理坐标、顶点法向量等。有太多这样的工具,此处无法一一列出,有 几个例子是MAYA、Blender、Lightwave、Cinema4D等。Blender是免费和开源的。图6.10显示了编辑3D模型时的Blender屏幕示例。
在这里插入图片描述

图6.10 Blender模型创建示例[BL16]

为了让我们在OpenGL场景中使用DCC工具创建的模型,需要以我们可以读取(导入)到我们程序中的格式保存(导出)该模型。有好几种标准的3D模型文件格式;再次说明,有太多无法一一列出,有一些例子是Wavefront(.obj)、3D Studio Max(.3ds)、斯坦福扫描存储库(.ply)、Ogre3D(.mesh)供参考。其中最简单的是 Wavefront(通常被称为OBJ),所以我们将仔细讲解它。

OBJ文件很简单,我们可以相对容易地开发一个基本的导入器。在 OBJ文件中,通过文本行的形式指定顶点几何数据、纹理坐标、法向量和其他信息。它有一些限制——例如,OBJ文件无法指定模型动画。

OBJ文件中的行,以字符标记开头,表示该行上的数据类型。一些 常见的标签包括:

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

还有其他标签可以用来存储对象名称、使用的材质、曲线、阴影和许多其他细节。我们这里只讨论上面列出的4个标签,这些标签足以导入各种复杂模型。

假设我们使用Blender构建一个简单的金字塔,例如我们为程序 4.3开发的金字塔。图6.11是在Blender中创建的类似的金字塔的屏幕截图。

在这里插入图片描述

图6.11 在Blender中构建的金字塔

在Blender中,如果我们现在导出我们的金字塔模型,指定.obj格 式,并设置Blender输出纹理坐标和顶点法向量,则会创建一个包含所 有这些信息的OBJ文件。生成的OBJ文件如图6.12所示。(纹理坐标的 实际值可能因模型的构建方式而异。)
在这里插入图片描述

图6.12 金字塔导出的OBJ文件

我们对OBJ文件的重要部分进行了颜色标记以供参考。顶部以 “#”开头的行是由Blender放置的注释,我们的导入器忽略了这些注释。接下来是以“o”开头的行,给出对象的名称。我们的导入器也可以忽略这一行。之后,有一行以“s”开头,指定表面不应该被平滑。 我们的代码也会忽略以“s”开头的行。

OBJ文件中的第一部分有实际内容的行是以“v”开头的那些(第4行~第8行)。它们指定金字塔模型的5个顶点相对于原点的X Y 和Z局部空间坐标。在这里,原点位于金字塔的中心。

红色的值(以“vt”开头)是各种纹理坐标。纹理坐标列表比顶点列表长的原因是一些顶点参与多个三角形,并且在这些情况下可能使用不同的纹理坐标。

绿色的值(以“vn”开头)是各种法向量。该列表通常也比顶点列表长(尽管在该示例中不是这样),同样是因为一些顶点参与多个三角形,并且在那些情况下可能使用不同的法向量。

在文件底部附近标记为紫色的值(以“f”开头)指定三角形(即 “面”)。在此示例中,每个面(三角形)具有3个元素,每个元素具有由“/”分隔的3个值(OBJ也允许其他格式)。每个元素的值分别是顶点列表、纹理坐标和法向量的索引。

例如,第三个面是:f 2 / 7 / 3 5 / 8 / 3 3 / 9 / 3 这表明顶点列表中的第2个、第5个和第3个顶点(蓝色)组成了一个三角形(请注意OBJ索引从1开始)。相应的纹理坐标是红色部分中纹理坐标列表中的第7项、第8项和第9项。所有3个顶点都具有相同的法向量,也就是以绿色显示的法向量列表中的第3项。

OBJ格式的模型并不要求具有法向量,甚至纹理坐标。如果模型没有纹理坐标或法向量,则面的数值将仅指定顶点索引:

f 2 5 3

如果模型具有纹理坐标,但不具有法向量,则格式如下:

f 2 / 7 5 / 8 3 / 9

并且,如果模型具有法向量但没有纹理坐标,则格式为:
f 2 / / 3 5 / / 3 3 / / 3

模型具有数万个顶点并不罕见。对于所有可以想象的应用场景, 几乎都可以在互联网上下载到数百种这样的模型,包括动物、建筑物、汽车、飞机、神话生物、人等。

在互联网上可以获得可以导入OBJ模型的复杂程序各不相同的导入程序。编写一个非常简单的OBJ加载器函数也并不困难,它可以处理我 们看到的基本标记(v、vt、vn和f)。程序6.3显示了一个这样的加载 器,尽管功能非常有限。它包含一个类来保存任意的导入模型,该模型又调用导入器。

在我们讲述简单OBJ导入器的代码之前,我们必须警告读者其局限性。

  • 它仅支持包含所有3个面属性字段的模型。也就是说,顶点位置、 纹理坐标和法向量都必须以f #/#/# #/#/# #/#/#这种形式存在。
  • 材质标签将被忽略——必须使用第5章中描述的方法完成纹理化。
  • 仅支持由单个三角形网格组成的OBJ模型(OBJ格式支持复合模型,但我们的简单导入器不支持)(复合模型加载器可以参考下面这个)。
  • 假设每行上的元素只用一个空格分隔。

https://blog.csdn.net/hyy_sui_yuan/article/details/104885628?spm=1001.2014.3001.5506
https://blog.csdn.net/qinyuanpei/article/details/49991607?spm=1001.2014.3001.5506

如果您的OBJ模型不满足上述所有条件,并且您希望使用程序6.3 中的简单加载程序导入它,则可能仍然可行。通常可以将这样的模型加载到Blender中,然后将其导出到另一个满足加载器限制条件的OBJ 文件中。例如,如果模型不包含法向量,则可以让Blender在导出修改后的OBJ文件时生成法向量。

我们的OBJ加载器的另一个限制与索引有关。在前面的描述中提到 了“f”标签允许混合和匹配顶点位置、纹理坐标和法向量的可能性。 例如,两个不同的“面”行可以包括指向相同v条目但是不同vt条目的索引。遗憾的是,OpenGL的索引机制不支持这种灵活性——OpenGL中的索引条目只能指向特定的顶点及其属性。这使得在某种程度上编写OBJ模型加载器变得复杂,因为我们不能简单地将三角形面条目中的引用复制到索引数组中。相反,使用OpenGL索引需要确保面条目的v、vt 和vn值的整个组合在索引数组中都有自己的引用。一种更简单但效率 更低的替代方案是为每个三角形面条目创建一个新顶点。尽管使用 OpenGL索引具有节省空间的优势(特别是在加载较大模型时),但为了清晰,我们选择这种更简单的方法。

ModelImporter类包含一个parseOBJ()函数,它逐行读取OBJ文 件,分别处理v、vt、vn和f这4种情况。在每种情况下,提取行上的后续数字,首先使用erase()跳过初始的v、vt、vn或f字符,然后使用 C++ stringstream类的“>>”运算符提取每个后续参数值,然后将它们存储在C++浮点向量中。当处理面(f)条目时,使用C++浮点向量中的对应条目构建顶点,包括顶点位置、纹理坐标和法向量。

ModelImporter类和ImportedModel类包含在同一个文件中, ImportedModel类通过将导入的顶点放入vec2和vec3对象的向量中,简化了加载和访问OBJ文件顶点的过程。回想一下这些GLM类;我们在这 里使用它们来存储顶点位置、纹理坐标和法向量。然后, ImportedModel类中的读取函数使它们可用于C++/OpenGL应用程序,其 方式与Sphere和Torus类中的方式相同。

在ModelImporter和ImportedModel类之后是一系列调用示例,加 载OBJ文件,然后将顶点信息传输到一组VBO中以供后续渲染。

图6.13显示了从NASA网站[NA16]下载的OBJ格式的航天飞机渲染模 型,使用程序6.3中的代码导入,并使用程序5.1中的代码和相应的带有各向异性过滤的NASA纹理图像文件进行纹理化。该纹理图像是使用 UV映射的示例,其中模型中的纹理坐标被仔细地映射到纹理图像的特 定区域。(如第5章所述,UV映射的细节超出了本书的范围。)
在这里插入图片描述

图6.13 带有纹理的NASA航天飞机模型

程序6.3 简化的(有限制的)OBJ加载器
vertShader.glsl

#version 430

layout (location = 0) in vec3 position;
layout (location = 1) in vec2 tex_coord;
out vec2 tc;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D s;

void main(void)
{
    
    	gl_Position = proj_matrix * mv_matrix * vec4(position,1.0);
	tc = tex_coord;
}

fragShader.glsl

#version 430

in vec2 tc;
out vec4 color;

uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (binding=0) uniform sampler2D s;

void main(void)
{
    
    
	color = texture(s,tc);
}

ImportedModel.h

#include <vector>
//通过将导入的顶点放入vec2和vec3对象的向量中,简化了加载和访问OBJ文件顶点的过程
class ImportedModel
{
    
    
private:
	// 从OBJ文件读取的数值
	int numVertices;
	// 保存为顶点属性以供后续使用的数值
	std::vector<glm::vec3> vertices;
	std::vector<glm::vec2> texCoords;
	std::vector<glm::vec3> normalVecs;
public:
	ImportedModel();
	ImportedModel(const char *filePath);
	int getNumVertices();
	std::vector<glm::vec3> getVertices();
	std::vector<glm::vec2> getTextureCoords();
	std::vector<glm::vec3> getNormals();
};

class ModelImporter
{
    
    
private:
	std::vector<float> vertVals;
	std::vector<float> triangleVerts;
	std::vector<float> textureCoords;
	std::vector<float> stVals;
	std::vector<float> normals;
	std::vector<float> normVals;
public:
	ModelImporter();
	void parseOBJ(const char *filePath);//逐行读取OBJ文 件,分别处理v、vt、vn和f这4种情况
	int getNumVertices();
	std::vector<float> getVertices();
	std::vector<float> getTextureCoordinates();
	std::vector<float> getNormals();
};

ImportedModel.cpp

#include <fstream>
#include <sstream>
#include <glm\glm.hpp>
#include "ImportedModel.h"
using namespace std;

ImportedModel::ImportedModel() {
    
    }

ImportedModel::ImportedModel(const char *filePath) {
    
    
	ModelImporter modelImporter = ModelImporter();
	modelImporter.parseOBJ(filePath); 使用modelImporter获取顶点信息
	numVertices = modelImporter.getNumVertices();
	std::vector<float> verts = modelImporter.getVertices();
	std::vector<float> tcs = modelImporter.getTextureCoordinates();
	std::vector<float> normals = modelImporter.getNormals();

	for (int i = 0; i < numVertices; i++) {
    
    
		vertices.push_back(glm::vec3(verts[i*3], verts[i*3+1], verts[i*3+2]));
		texCoords.push_back(glm::vec2(tcs[i*2], tcs[i*2+1]));
		normalVecs.push_back(glm::vec3(normals[i*3], normals[i*3+1], normals[i*3+2]));
	}
}

int ImportedModel::getNumVertices() {
    
     return numVertices; }
std::vector<glm::vec3> ImportedModel::getVertices() {
    
     return vertices; }
std::vector<glm::vec2> ImportedModel::getTextureCoords() {
    
     return texCoords; }
std::vector<glm::vec3> ImportedModel::getNormals() {
    
     return normalVecs; }

// ---------------------------------------------------------------

ModelImporter::ModelImporter() {
    
    }

//逐行读取OBJ文 件,分别处理v、vt、vn和f这4种情况
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) {
    
    // 顶点位置("v"的情况)
			stringstream ss(line.erase(0, 1)); //使用erase()跳过初始的v、vt、vn或f字符
			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) {
    
    // 三角形面("f"的情况)
			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;// "stoi"将字符串转化为整型
				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]);
			}
		}
	}
}
int ModelImporter::getNumVertices() {
    
     return (triangleVerts.size()/3); }
std::vector<float> ModelImporter::getVertices() {
    
     return triangleVerts; }
std::vector<float> ModelImporter::getTextureCoordinates() {
    
     return textureCoords; }
std::vector<float> ModelImporter::getNormals() {
    
     return normals; }

main.cpp

#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <cmath>
#include <glm\glm.hpp>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "ImportedModel.h"
#include "Utils.h"
using namespace std;

#define numVAOs 1
#define numVBOs 3

float cameraX, cameraY, cameraZ;
float objLocX, objLocY, objLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
GLuint vbo[numVBOs];
GLuint shuttleTexture;

// variable allocation for display
GLuint mvLoc, projLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvMat;
// 在顶层声明中使用模型导入器
ImportedModel myModel("shuttle.obj");

float toRadians(float degrees) {
    
     return (degrees * 2.0f * 3.14159f) / 360.0f; }

void setupVertices(void) {
    
    
 	std::vector<glm::vec3> vert = myModel.getVertices();
	std::vector<glm::vec2> tex = myModel.getTextureCoords();
	std::vector<glm::vec3> norm = myModel.getNormals();

	std::vector<float> pvalues;
	std::vector<float> tvalues;
	std::vector<float> nvalues;

	for (int i = 0; i < myModel.getNumVertices(); i++) {
    
    
		pvalues.push_back((vert[i]).x);
		pvalues.push_back((vert[i]).y);
		pvalues.push_back((vert[i]).z);
		tvalues.push_back((tex[i]).s);
		tvalues.push_back((tex[i]).t);
		nvalues.push_back((norm[i]).x);
		nvalues.push_back((norm[i]).y);
		nvalues.push_back((norm[i]).z);
	}

	glGenVertexArrays(1, vao);
	glBindVertexArray(vao[0]);
	glGenBuffers(numVBOs, vbo);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[0]);
	glBufferData(GL_ARRAY_BUFFER, pvalues.size() * 4, &pvalues[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[1]);
	glBufferData(GL_ARRAY_BUFFER, tvalues.size() * 4, &tvalues[0], GL_STATIC_DRAW);

	glBindBuffer(GL_ARRAY_BUFFER, vbo[2]);
	glBufferData(GL_ARRAY_BUFFER, nvalues.size() * 4, &nvalues[0], GL_STATIC_DRAW);
}

void init(GLFWwindow* window) {
    
    
	renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl");
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 1.5f;
	objLocX = 0.0f; objLocY = 0.0f; objLocZ = 0.0f;

	glfwGetFramebufferSize(window, &width, &height);
	aspect = (float)width / (float)height;
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);

	setupVertices();
	shuttleTexture = Utils::loadTexture("spstob_1.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(objLocX, objLocY, objLocZ));

	mMat = glm::rotate(mMat, 0.0f, glm::vec3(1.0f, 0.0f, 0.0f));
	mMat = glm::rotate(mMat, toRadians(135.0f), glm::vec3(0.0f, 1.0f, 0.0f));
	mMat = glm::rotate(mMat, toRadians(35.0f), glm::vec3(0.0f, 0.0f, 1.0f));

	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, shuttleTexture);

	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LEQUAL);
	glDrawArrays(GL_TRIANGLES, 0, myModel.getNumVertices());
}

void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
    
    
	aspect = (float)newWidth / (float)newHeight;
	glViewport(0, 0, newWidth, newHeight);
	pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}

int main(void) {
    
    
	if (!glfwInit()) {
    
     exit(EXIT_FAILURE); }
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	GLFWwindow* window = glfwCreateWindow(600, 600, "Chapter6 - program1", NULL, NULL);
	glfwMakeContextCurrent(window);
	if (glewInit() != GLEW_OK) {
    
     exit(EXIT_FAILURE); }
	glfwSwapInterval(1);

	glfwSetWindowSizeCallback(window, window_size_callback);

	init(window);

	while (!glfwWindowShouldClose(window)) {
    
    
		display(window, glfwGetTime());
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	glfwDestroyWindow(window);
	glfwTerminate();
	exit(EXIT_SUCCESS);
}

猜你喜欢

转载自blog.csdn.net/weixin_44848751/article/details/130900403