定睛一看,已经12月了😵💫。
本来计划图形学基础总共写5个A类大作业,结果发现期末根本应付不过来。
看看剩下几个看着就秃头的A类作业(物理引擎、光追和网格加密&简化),还是先写B类把作业交上再说😪😪😪
这篇也是图形学的小作业,主要实现了半边结构网格的构建和读取。
实验原理
使用半边结构读入obj格式的三维网格模型,并配置VAO、EBO;通过glPolygenMode()
设置点、线、面等显示模式。
半边结构
半边结构(The Half-Edge Data-Structure)是一种适用于流形多边形网格的数据结构。半边结构将一条边分解为两条半边(half-edge),二者方向相反,各自属于相邻面的边环。其优点是各种查找的复杂度都只有O(1)。
-
半边结构中的顶点
Vertex
-
存储自身坐标
vec3(x,y,z)
- 存储出边(out-edge)指针
Halfedge*
出边一般为网格的边界。
在本次实验读取网格时,通过半边终点的出边查找twin半边是否存在。
-
-
半边结构中的半边
Halfedge
- 存储起点指针
Vertex*
- 存储所属面指针
Face*
- 存储上一条半边指针
Halfedge*
- 存储下一条半边指针
Halfedge*
- 存储相反的半边指针
Halfedge*
- 存储起点指针
-
半边结构中面
Face
- 存储第一条半边指针
Halfedge*
- 存储第一条半边指针
点&线&面的绘制
通过glPolygonMode()
实现了点(GL_POINT
)、线(GL_LINE
)、面(GL_FILL
)绘制的切换。要点如下:
- 可编程管线下的shader配置:通过
uniform vec3
设置不同颜色,实现一shader多用; - 点绘制:启用
GL_VERTEX_PROGRAM_POINT_SIZE
,可以于顶点着色器中通过gl_PointSize
设置点模式下点的大小; - 线绘制:作业样例中的网格显示只包含正面线框,但实验发现若在
glPolygonMode()
中配置只绘制正面(GL_FRONT
)似乎不适用于可编程管线,因此通过启用GL_CULL_FACE
以实现反面的剔除。 - 面+线绘制:先以
GL_FILL
模式绘制面,再以GL_LINE
模式绘制线框。注意如果开启了深度测试,绘制的线会收到一定面片的遮挡,可以绘制线时关闭深度测试或将顶点向外进行一个小的偏移。
实验步骤
1.构造数据结构(见mesh.h
)
除前面提到的Vertex
、Halfedge
、Face
等结构外,EdgeKey
结构用于构建哈希表,便于创建网格时查找半边是否已经存在。另外,Mesh
类中indices
、VAO
、VBO
、EBO
用于可编程管线的网格绘制,不是半边结构的构成部分。
Mesh
类中保存了顶点表、半边表和面表,其中后两者都是保存的指针,但顶点直接保存的结构体。这是因为struct
的属性在内存中的存储顺序是连续的,可以直接将其作为AttributeArray
设置给VAO
,便于绘制。
2.读取文件(见Mesh::loadFile()
)
逐行读取obj文件。
obj 文件每一行为一条信息,开头字母代表信息类型;
v 开头为顶点,三个值分别为x,y,z坐标;
f 开头为顶点, 三个值为点索引。注意这里的索引值从1开始计数,注意使用时-1以防止数组越界。
void Mesh::loadFile(std::string path)
{
std::ifstream objFile;
objFile.open(path);
std::string line, keyword;
std::stringstream stream;
float x, y, z;
int vert0, vert1, vert2;
while (objFile && !objFile.eof())
{
std::getline(objFile, line);
if (line.size() == 0 || line[0] == '#' || isspace(line[0]))
continue;
stream.str(line);
stream.clear();
stream >> keyword;
//load vertex
if (keyword == "v") {
stream >> x >> y >> z;
addVertex(x, y, z);
}
//load faces
else if (keyword == "f") {
stream >> vert0 >> vert1 >> vert2;
addFace(--vert0, --vert1, --vert2);
}
}
objFile.close();
setupMesh();
}
3.创建顶点(见Mesh::addVertex()
)
首先通过坐标逐个创建顶点,出边outedge*
暂时为空。
Vertex* Mesh::addVertex(float x, float y, float z)
{
Vertex* vertex= new Vertex(x, y, z);
vertices.push_back(*vertex);
return vertex;
}
4.创建三角面(见Mesh::addFace()
)
全部顶点读取完成后,开始逐个创建三角面和半边。
创建三角面时,还需要构建面内半边之间的连接关系。因此首先我们要获取面内的三条半边:
4.1创建半边对(见Mesh::addEdge()
)
所有半边都是成对出现的,也会成对创建。因此有可能我们所需的半边在先前已经创建过了,为获知v1->v2的半边是否已经存在,我使用unordered map来保存顶点对和半边间的关系。并在创建半边前,首先查询哈希表,若半边存在则直接返回该半边。
Halfedge* Mesh::addEdge(Vertex* v1, Vertex* v2) {
//查找v1-v2半边是否已经存在
EdgeKey _key(v1->index, v2->index);
if (hashmap.find(_key) != hashmap.end()) {
return hashmap[_key];
}
//若不存在,创建新的半边对
Halfedge* innerEdge = new Halfedge(v1);
edges.push_back(innerEdge);
Halfedge* outterEdge = new Halfedge(v2);
edges.push_back(outterEdge);
innerEdge->twin = outterEdge;
outterEdge->twin = innerEdge;
v1->outEdge = innerEdge;
v2->outEdge = outterEdge;
//存入哈希表
hashmap[EdgeKey(v1->index, v2->index)] = innerEdge;
hashmap[EdgeKey(v2->index, v1->index)] = outterEdge;
return innerEdge;
}
4.2 构建半边间连接关系
设置各半边的next指针与Face的startEdge指针。
Face* Mesh::addFace(int v0, int v1, int v2)
{
Face* face = new Face();
Vertex* vertexs[3];
vertexs[0] = &vertices[v0];
vertexs[1] = &vertices[v1];
vertexs[2] = &vertices[v2];
Halfedge* _edges[3];
for (int i = 0; i < 3; i++) {
_edges[i] = addEdge(vertexs[i % 3], vertexs[(i + 1) % 3]);
}
for (int i = 0; i < 3; i++) {
_edges[i]->next = _edges[(i + 1) % 3];
_edges[i]->incFace = face;
}
face->startEdge = _edges[0];
faces.push_back(face);
return face;
}
5.配置网格(见Mesh::setupMesh()
)
由于使用可编程管线,需要为网格配置VAO、VBO、EBO。
前文提到vertices
保存的是Vertex struct
本体,所以position
、normal
这些信息在内存空间中是连续的。
在设置VertexAttributeArray
时,可以通过struct的offsetof()
方法获取属性的开始位置,例如:
// 法线
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
(ps.这里卡了好久...debug的时候给Vertex加了一个index属性,还写在了最前面,然后就完全忘记这回事,结果后来就渲染不出来了....因为position的Pointer写的(void*)0.....)
void Mesh::setupMesh() {
computeIndices();
computeNormals();
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
&indices[0], GL_STATIC_DRAW);
// 顶点位置
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);
// 法线
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
glBindVertexArray(0);
}
6.按不同模式绘制(见Mesh::draw()
)
void Mesh::draw()
{
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, static_cast<unsigned int>(indices.size()), GL_UNSIGNED_INT, 0);
}
实验效果
四种绘制效果如下。通过键盘的1、2、3、4切换四种绘制模式。
实验环境
Window 10. Visual Studio 2022. 依赖库:GLFW,GLAD, GLM