【图形学实验】openGL使用Laplacian进行网格光顺

目录

 

obj文件的数据结构

读取obj文件

绘制模型

Laplacian光顺

demo


最近一个题目要求使用openGL,采用Laplacian算子对obj文件进行网格光顺。只是简单的接触一下,算是作业的记录吧。

什么是网格光顺?网格光顺在不改变顶点之间连接关系、减小曲率变化的前提下移动顶点,达到减小噪声、改善网格三角形形状的效果。该善三角形形状->趋向于正三角形,你可以观察下面的三角形网格变化。

先用gif图演示一下网格光顺的效果,直观感受一下

上一次接触Laplacian算子还是在数字图像处理的课程上,使用Laplacian算子处理图像来获得边缘增强的图像,再与原来的图像进行结合,实现图像的锐化。

obj文件的数据结构

开始先使用MeshLab看看我们要处理的obj文件

用记事本打开obj文件可以看到这个obj文件里面只有

v:几何体顶点(Geometry Vertice)

vn:顶点法线(Vertex Normal)

f:面(Face)

三种数据

截取部分:

其中v和vn后面跟的都是xyz

usemtl(null)表示没有贴图

面f的数据格式是 

f 顶点索引//顶点法向索引  顶点索引//顶点法向索引  顶点索引//顶点法向索引

比如 “f 1//4 2//5 3//6” 就是这个面由第1、2、3个点构成,这三个顶点又分别对应第4、5、6个顶点法向 

(如果有贴图的话就是  顶点索引/纹理索引/顶点法向索引)

注意,obj文件的索引都是从1开始的而不是0

读取obj文件

代码参考了这位博主的代码:非常简单,教你用OpenGL读入obj模型_旧时光 | YoungChen's 博客-CSDN博客

在其基础上进行了改进:

1、原po的顶点法向是计算出来的,而我们的obj文件中已经有顶点法向的数据,添加了读取顶点法向的操作

2、同时存储顶点xyz坐标的vset中还记录了包含该顶点的face,将face索引存入顶点的vector中是为了便于之后Laplacian的相关计算,我不知道obj文件的顶点之间是否有什么特殊的排列或者什么办法能够快速地找到周围的点。

关于我读代码的时候的一点疑问(其实后来自己写的时候也就明白了):

当getline读取每一行后,为什么要在这一行后面添加一个空格 " "  ?

因为在将字符串拆根据空格拆分成几部分的过程中是根据空格作为分隔符的,当遇到分隔符空格时,会将前面的字符串存储起来。而getline获得的字符串末尾是回车符,比如:"a  bc  de  gf"只能被存储成a、bc、de三部分,因为一直到字符串末尾都没有再遇到空格,所以不会触发相应的操作将gf存储起来。

objLoader::objLoader(string filename)
{
	drawmode = GL_LINE;//默认是绘制轮廓
	string line;//读取每一行
	fstream f;
	f.open(filename, ios::in);//读文件
	if (!f.is_open()) {
		cout << "file cannot open" << endl;
	}
	else {
		cout << "file open successful" << endl;
	}

	while (!f.eof()) {
		getline(f, line);
		char breakmarker = ' ';//obj中每行各部分以空格隔开
		string little_tail = " ";
		line = line.append(little_tail);//为line末尾添加一个分隔符
		//将当前行根据空格分开
		string part = "";
		vector<string> partofline;//当前行根据空格拆分成的各个string部分
		for (int i = 0; i < line.length(); i++) {
			char nowchar = line[i];
			if (nowchar == breakmarker) {
				//如果当前char为空格,将前面的部分存入partofline
				partofline.push_back(part);
				part = "";
			}
			else {
				part += nowchar;
			}
		}
		//当前行拆分完毕
		//根据不同的数据进行相应操作
		if (partofline.size() == 4) {
			//我们的网格是三角形网格,如果不是拆分成4部分那就没必要检查
			if (partofline[0] == "v") {
				//当前行是顶点,存储顶点坐标
				vector<GLdouble> v;
				for (int n = 1; n < 4; n++) {
					//GLdouble xyz = atof(partofline[n].c_str());//转换成double
					GLdouble xyz = atof(partofline[n].c_str());//转换成double
					v.push_back(xyz);
				}
				vset.push_back(v);
			}
			if (partofline[0] == "vn") {
				//当前行是顶点向量,存储向量
				vector<GLdouble> vn;
				for (int n = 1; n < 4; n++) {
					//GLdouble xyz = atof(partofline[n].c_str());//转换成double
					GLdouble xyz = atof(partofline[n].c_str());//转换成double
					vn.push_back(xyz);
				}
				vnset.push_back(vn);
			}
			
			if (partofline[0] == "f") {
				//当前行是面,将顶点索引和对应的顶点法向索引存储起来
				//因为在执行到面的时候所有顶点已经存储完成,将顶点对应的面存储到顶点中
				vector<GLint> f;
				//因为数据格式是  顶点索引//顶点法向索引
				//要将中间两道斜线去掉
				for (int n = 1; n < 4; n++) {//处理三个v//vn
					vector<string> vvn;//存放处理好的v和vn
					string v_and_vn = partofline[n];// v//vn
					v_and_vn = v_and_vn.append("/");
					string part = "";
					for (int c = 0; c < v_and_vn.length(); c++) {//将v//vn拆开
						char nowcharofpart = v_and_vn[c];
						//因为2个/,vvn[0]存储v,vvn[1]存储"",vvn[2]存储vn
						if (nowcharofpart == '/') {
							vvn.push_back(part);
							part = "";
						}
						else {
							part += nowcharofpart;
						}
					}
					//v和vn已经拆分好并存入了vvn,现在存入对应的set
					GLint vindex = atof(vvn[0].c_str());
					GLint vnindex = atof(vvn[2].c_str());
					//f的结构为 { v0 , vn0 , v1 , vn1 , v2 , vn2 }
					f.push_back(vindex);
					f.push_back(vnindex);
					//cout << "v:" << vindex << endl;
					//cout << "vn:" << vnindex << endl;
					//既然已经知道了顶点索引,将面索引存入到对应的顶点中去
					//obj文件的索引是从1开始的而不是0
					//这时候f还没有添加到fset中去,f的序号(从0开始)应该为fset.size()
					vset[vindex - 1].push_back(fset.size());
					//cout << "face number:" << fset.size()<<endl;
				}

				fset.push_back(f);//将顶点索引和顶点向量索引存入fset
			}
		}
		/*
		//输出当前行的初步拆分结果
		cout<<"line:" << line << endl;
		cout << "part number:" << partofline.size()<<endl;
		for (int j = 0; j < partofline.size(); j++) {
			cout << partofline[j] << endl;
		}
		*/
	}

	vset_begin.assign(vset.begin(), vset.end());//将vset备份
	f.close();
	/*
	//输出最终结果
	cout << "输出vset" << endl;
	for (int m = 0; m < vset.size(); m++) {
		for (int n = 0; n < vset[m].size(); n++) {
			cout << vset[m][n] << ",";
		}
		cout << endl;
	}
	cout << "输出vnset" << endl;
	for (int m = 0; m < vnset.size(); m++) {
		for (int n = 0; n < vnset[m].size(); n++) {
			cout << vnset[m][n] << ",";
		}
		cout << endl;
	}
	cout << "输出fset" << endl;
	for (int m = 0; m < fset.size(); m++) {
		for (int n = 0; n < fset[m].size(); n++) {
			cout << fset[m][n] << ",";
		}
		cout << endl;
	}
	*/
}

绘制模型

glNormal和glVertex将读取到的顶点法向与对应的顶点绑定

void objLoader::drawobj() {
	int vindex1, vindex2, vindex3,
		vnindex1,vnindex2,vnindex3;
		glPolygonMode(GL_FRONT_AND_BACK, drawmode);
		for (int i = 0; i < fset.size(); i++) {
			//索引从1开始,set从0开始
			//顶点索引
			vindex1 = fset[i][0] - 1;
			vindex2 = fset[i][2] - 1;
			vindex3 = fset[i][4] - 1;
			//顶点法向索引
			vnindex1 = fset[i][1] - 1;
			vnindex2 = fset[i][3] - 1;
			vnindex3 = fset[i][5] - 1;
			glBegin(GL_TRIANGLES);
			if (reverseVertexNormal) {
				glNormal3d(-vnset[vnindex1][0], -vnset[vnindex1][1], -vnset[vnindex1][2]);//设置顶点法向
				glVertex3d(vset[vindex1][0], vset[vindex1][1], vset[vindex1][2]);
				glNormal3d(-vnset[vnindex2][0], -vnset[vnindex2][1], -vnset[vnindex2][2]);
				glVertex3d(vset[vindex2][0], vset[vindex2][1], vset[vindex2][2]);
				glNormal3d(-vnset[vnindex3][0], -vnset[vnindex3][1], -vnset[vnindex3][2]);
				glVertex3d(vset[vindex3][0], vset[vindex3][1], vset[vindex3][2]);
			}
			else {
				glNormal3d(vnset[vnindex1][0], vnset[vnindex1][1], vnset[vnindex1][2]);//设置顶点法向
				glVertex3d(vset[vindex1][0], vset[vindex1][1], vset[vindex1][2]);
				glNormal3d(vnset[vnindex2][0], vnset[vnindex2][1], vnset[vnindex2][2]);
				glVertex3d(vset[vindex2][0], vset[vindex2][1], vset[vindex2][2]);
				glNormal3d(vnset[vnindex3][0], vnset[vnindex3][1], vnset[vnindex3][2]);
				glVertex3d(vset[vindex3][0], vset[vindex3][1], vset[vindex3][2]);
			}
			glEnd();
		}
	
}

glPolygonMode()的第二个参数通过在GL_LINE和GL_FILL切换可以达到不同的绘制效果,左为GL_LINE,右为GL_FILL

题外话:这里面的reverseVertexNormal是我自己加的一个布尔量,因为这里的顶点法向是从文件中读取的,如果是自己计算顶点法向有可能得到的结果与实际结果相反。于是我就想试试如果把顶点法向反过来会是什么样子,试验结果也贴一下:

为了使看到的模型有亮有暗我调整了一下光源的位置,且环境光几乎为0,demo中应该看不到这样明显的效果,可能需要手动调整一下光源位置并关闭环境光

左为原顶点法向的模型,右为将顶点法向变为相反方向之后的模型,可以看到原来亮的部分变暗了,暗的部分变亮了。

Laplacian光顺

我们在进行网格光顺时并没有考虑权重对新坐标的影响,如果引入权重光顺的效果会更好一些。这里只是浅显地实验一下

光顺过程中不能直接用新的顶点去替换别的顶点,因为可能还有别的顶点要使用这个顶点来计算。

容易出错的一处地方是不要忘记将旧顶点的face序号存入新顶点中,否则只能进行一次Laplacian光顺,第二次光顺时就会因为找不到face序号而无法计算。

//计算拉普拉斯算子进行网格光顺后的点
void objLoader::LaplacianCal() {
	for (int i = 0; i < vset.size(); i++) {
		//找到顶点周围的所有点的序号
		vector<GLint> v_near;//存放周围点的序号
		
		for (int j = 3; j < vset[i].size(); j++) {
			//遍历包含该顶点的所有face
			//因为vset存储的是double,得到序号需要转换成int
			//vset[012]存放顶点,之后存放的是face的索引(从0开始)
			int faceindex = (int)vset[i][j];
			for (int k = 0; k < 5; k = k+2) {//遍历face的三个顶点索引,注意fset  { v0 , vn0 , v1 , vn1 , v2 , vn2 }
				//检查是否与当前顶点是一个顶点,face中存储的序号从1开始
				if (fset[faceindex][k]-1!=i ) {
					//不是的话加入v_near
					v_near.push_back(fset[faceindex][k]-1);
				}
			}
		}
		
		//计算拉普拉斯分量乘以参数
		double xsum = 0, 
				ysum = 0,
				zsum = 0;
		for (int m = 0; m < v_near.size(); m++) {
			int vindex = v_near[m];
			xsum += vset[vindex][0];
			ysum += vset[vindex][1];
			zsum += vset[vindex][2];
		}
		int numofv = v_near.size();
		double newx = xsum / numofv*para_ofLaplacian + (1-para_ofLaplacian)*vset[i][0],
			newy = ysum / numofv * para_ofLaplacian + (1 - para_ofLaplacian)*vset[i][1],
			newz = zsum / numofv * para_ofLaplacian + (1 - para_ofLaplacian)*vset[i][2];
		vector<GLdouble> newv;
		newv.push_back(newx);
		newv.push_back(newy);
		newv.push_back(newz);
		for (int n = 3; n < vset[i].size(); n++) newv.push_back(vset[i][n]);//不要忘记将face序号也存储进去,准备下一次拉普拉斯算子的计算
		//存储新顶点
		vset_afterLaplacian.push_back(newv);
		
	}
	vset.clear();
	vset.assign(vset_afterLaplacian.begin(), vset_afterLaplacian.end());
	vset_afterLaplacian.clear();
}

看一下Laplacian算子进行光顺之后的效果(左为原模型,右为Laplacian系数设置为0.5,光顺3次之后的结果)

demo

demo还实现了使用鼠标左键拖拽,右键旋转、滚轮缩放的功能,支持绘制模式在GL_LINE和GL_FILL之间切换,反转顶点法向。交互做的比较糙,虽然拖拽只能在XOZ平面上拖拽,旋转只能绕Z轴和Y轴旋转,但结合起来基本上模型哪个部分都能看到(可能旋转之后有些别扭就是了),关于使用main文件里面有说明

openGL使用Laplacian算子进行网格光顺

  • 4
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值