OpenGL

🕹️OpenGL丨半边结构模型读取&显示[1]

by ERIN.Z, 2022-12-02


定睛一看,已经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*

(图源cs184/284a) slide-22.jpg

点&线&面的绘制

通过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) Snipaste_2022-12-05_11-56-47.jpg 除前面提到的VertexHalfedgeFace等结构外,EdgeKey结构用于构建哈希表,便于创建网格时查找半边是否已经存在。另外,Mesh类中indicesVAOVBOEBO用于可编程管线的网格绘制,不是半边结构的构成部分。

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本体,所以positionnormal这些信息在内存空间中是连续的。 在设置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切换四种绘制模式。

result.jpg

实验环境

Window 10. Visual Studio 2022. 依赖库:GLFW,GLAD, GLM

by ERIN.Z

2024 © typecho & elise