[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。

20220829 更新了一下git库,修复了一些我也不知道的bug


代码基于LearnOpenGL的模型导入篇章的代码模板实现。

OpenGL 3.3
VS2019

代码:https://github.com/LoserFight/LoopSubdivision

参考:

半边结构参考:【图形学】Loop细分算法及半边结构实现(C++)

原理部分:GAMES101

实现的功能

  • 仅显示顶点,边或者面的切换,空格键切换

  • loop subdivision 细分,键盘右键→

  • 细分后保存同目录下的obj文件,按下q键

  • 支持二进制stl和obj格式的导入


  • wasd移动摄像机,配合鼠标移动视角

  • 按住键盘上的X Y或Z,上下移动鼠标可以旋转模型

  • ./xxx.exe 模型的目录/模型 可以打开指定模型

结果截图

细分:

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第1张图片

导出的模型:

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第2张图片

仅显示顶点:

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第3张图片

实现的思路

因为Loop细分需要一个顶点周围点的信息,所以选择了能较容易得到周围点、边、面的半边数据结构存储存储三角形网格。

什么是半边数据结构?

一般的3D模型文件会包含里面所有点或者面的相关信息,半边数据结构很巧妙,每个三角形可以用用三个顺时针或逆时针的向量边表示,因为是单向所以是半边。
一个半边数据结构的结构体应该包含

  1. 该半边指向的顶点
  2. 该半边的三角形的下一个半边
  3. 与该半边相反的半边,即相邻三角形的一条半边
    具体见文章开头地址的源码

半边数据结构的建立

基本思路是先导入需要的顶点数据,再用三角形的三个顶点构造面和半边。

如下所示的face构造,已经完成了必要顶点vertexs[3]的导入,在这个函数中将三条半边用next连接起来,实际上这个操作才是确定了一个面的三个面和三个顶点的所有信息,这步操作后,只需要一个半边或一个顶点就能得到所有关于三角形的数据。

//输入一个三角形的三个顶点数据,生成半边和面
Face* TriMesh::createFace(Vert* vertexs[3]) {
	Face* face = new Face();
	HalfEdge* edges[3];
	//建立一个三角形的半边
	for (int i=0; i < 3; i++) {
		edges[i] = createEdge(vertexs[i % 3], vertexs[(i + 1) % 3]);
		if (edges[i] == NULL) {
			std::cout << "Create Face went wrong" << std::endl;
			return NULL;
		}

	}
	//将对边通过next连接起来,并和该face绑定
	for (int i = 0; i < 3; i++) {
		edges[i]->next = edges[(i + 1) % 3];
		edges[i]->face = face;
		m_edges.push_back(edges[i]);
	}

	//给面赋值任意一个半边
	face->halfEdge = edges[0];
	m_faces.push_back(face);
	return face;
}

举个例子

下面是一个输入顶点地址,返回一个从顶点出发的半边vector

std::vector<HalfEdge*> TriMesh::getEdgesFromVertex(const Vert* vertex) {
	std::vector<HalfEdge*> result;
	HalfEdge* he = vertex->halfEdge;//he为从vertex出发的半边
	HalfEdge* temp = he;
	do {
		if (temp->isBoundary) {
			result.push_back(temp);
			break;
		}
		result.push_back(temp);
	} while (temp != he);
	
	if (temp->isBoundary) {
		temp = he->next->next;
		//换个方向
		std::reverse(result.begin(), result.end());
		do {
			if (temp->isBoundary || temp == NULL) {
				break;
			}
			result.push_back(temp->opposite);
			temp = temp->opposite->next->next;

		} while (temp->vert->id == vertex->id);//true
		//换回来
		std::reverse(result.begin(), result.end());

	}
	
	return result;

}

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第4张图片

可以看这张图,输入P,应该输出V0,V1,V2,V3.

文件导入

obj格式导入较容易,因为文件内就是用点的引索表示面。

stl就有点复杂,因为它表示 面时候是直接给出了三个顶点的坐标,有些点是重复的。

所以我建立了一个顶点坐标到顶点引索的表。当这个顶点不存在表内,说明是新的点,构造半边结构的点,否则返回已经存在表内对应顶点的索引值。

//model.h

//哈希操作
struct VKeyHashFuc 
    {
        std::size_t operator()(const glm::vec3& key) const
        {
            return std::hash<float>()(key.x) + 
                std::hash<float>()(key.y)+ 
                std::hash<float>()(key.z);
        }
    };

int loadStl(string const& path) {
        ifstream targetStl(path,ios::in|ios::binary);
        string line;
        char name[80];
        int v_id = 0;
        unsigned int triangles;
        //float normal;
    //声明一个哈希表
        std::unordered_map<glm::vec3, int, VKeyHashFuc> hashmap_vid;
        if (!targetStl) {
            cout << "Open .stl went wrong" << endl;
            return 0;
        }
        targetStl.read(name, 80);
        targetStl.read((char*)&triangles, sizeof(unsigned int));
        if (triangles == 0)
            return -1;
        for (unsigned int i = 0; i < triangles; i++) {
            float XYZ[12];//4*3
            int face[3];
            targetStl.read((char*)XYZ, 12 * sizeof(float));
            for (int j = 1; j < 4; j++) {
                glm::vec3 v = { XYZ[j * 3],XYZ[j * 3 + 1],XYZ[j * 3 + 2] };
                if (hashmap_vid.find(v) != hashmap_vid.end()) {
                    //顶点已经存在
                    face[j - 1] = hashmap_vid[v];
                }
                else {//全新顶点数据,更新哈希表,将索引Vid存入面j,这样通过哈希表和face[j]就能寻找到对应的顶点的xyz坐标。
                    hashmap_vid[v] = v_id;
                    face[j - 1] = v_id;
                    TMeshOri->createVertex(v, v_id++); 
                    
                
                }
            }
            //生成面
            Vert* ve[3];
            auto& mV = TMeshOri->Vertexs();
            ve[0] = mV[face[0]];
            ve[1] = mV[face[1]];
            ve[2] = mV[face[2]];
            TMeshOri->createFace(ve);

            targetStl.read((char*)XYZ, 2);
        }
        targetStl.close();

        TMeshOri->createBoundary();
        TMeshOri->calculateNormal();

        this->meshes.push_back(TriToMesh(TMeshOri));
        return 0;

        

    }

这样就可以像obj一样构建半边结构了。

细分

先对旧点遍历,得到每个点周围的顶点,乘上相应系数得到新的顶点位置,加入新的半边结构内

在对旧边遍历,得到新增点的位置,加入新的半边结构中

对旧面遍历,用之前已经插入的新顶点将每个面拆成4个小面。

这时候就能体现出半边数据结构的优点了,很轻松就能得到周围点的信息。

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第5张图片

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第6张图片

不过本文中,关于Old Vertex 用的是Loop最初提出用三角函数的系数,

如果是边节上的新点或者旧点,计算方法完全不一样

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第7张图片

...		
    //以旧点为例
		if (!oriVertexs[i]->isBoundary) {
			auto neighborV = ori->getNeighborVertexs(oriVertexs[i]);
			int n = neighborV.size();
			float beta = (5.0 / 8.0 - pow(3.0 / 8.0 + 1.0 / 4.0 * std::cos(2.0 * My_PI / n), 2.0)) / n;
			for (int j=0; j < neighborV.size(); j++) {
				newPos += neighborV[j]->vcoord;

			}
			//计算even points
			newPos = newPos * beta;
			newPos += oriVertexs[i]->vcoord * (1 - n * beta);

		}
		else {
			auto BoundaryNV=ori->getBoundaryNeighborVertexs(oriVertexs[i]);
			newPos = 0.125f * (BoundaryNV[0]->vcoord + BoundaryNV[1]->vcoord) + 0.75f * oriVertexs[i]->vcoord;
		}
...

再生成所有新点和更新所有旧点时,这些新生成本质上是新加入的点,可以像半边数据结构的建立一样构造loop Subdivision后的点。

新的法线

https://pbr-book.org/3ed-2018/Shapes/Subdivision_Surfaces

生成新的法线用的方式是从这个网站得到,细分也很全,前文基本上也参照了其中内容

每次细分后, 都要根据新的顶点和面生成法线,这个法线和原格式的法线很有可能不一样。

[OpenGL] 利用半边数据结构,实现曲面细分算法Loop Subdivision,附代码。_第8张图片

不过我算的时候S X T得到的刚好相反,改成了T X S。

三种显示

对mesh类和model类,增加了相应的图元绘制,见model.h和mesh.h。

同时还要把半边结构转化成对应的数据格式,如网格画线,只需要传入线段对应顶点。如下

//Line
static Mesh TriToLine(TriMesh* T) {
        vector<Vertex> vertexs;
        vector<unsigned int> ind;

        auto& mv = T->Vertexs();

        for (int i = 0; i < mv.size(); i++) {
            Vertex newP;
            newP.Position = mv[i]->vcoord;
            newP.Normal = mv[i]->ncoord;
            vertexs.push_back(newP);
        }

        auto& mf = T->HalfEdges();

        for (int i = 0; i < mf.size(); i++) {
            HalfEdge* he = mf[i];
            ind.push_back(he->vert->id);
            ind.push_back(he->next->next->vert->id);
        }
        vector<Texture>      tex;
        return Mesh(vertexs, ind, tex);
    
    }

//Mesh
 static Mesh TriToMesh(TriMesh* T) {
        vector<Vertex> vertexs;
        vector<unsigned int> ind;
        auto& mv = T->Vertexs();

        for (int i = 0; i < mv.size(); i++) {
            Vertex newP;
            newP.Position=mv[i]->vcoord;
            newP.Normal = mv[i]->ncoord;
            vertexs.push_back(newP);
        }

       // unsigned int vi = 0;
        auto& mf = T->Faces();
        for (int i = 0; i < mf.size(); i++) {
            Face* f = mf[i];
            HalfEdge* he = f->halfEdge;
            do {
                ind.push_back(he->vert->id);
                he = he->next;

            
            } while (he != f->halfEdge);

        }
             vector<Texture>      tex;
        return Mesh(vertexs, ind, tex);

    }
//不需要画点,因为如果画点,只需要不加索引就可以了

     

你可能感兴趣的:(图形学,数据结构,算法,c++,图形学,opengl)