C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019


获取建模得到的三维模型并解析

STL****文件

STL (STereoLithography, 立体光刻)是由3D Systems软件公司创立、原本用于立体光刻计算机辅助设计软件的文件格式。它有一些事后诸葛的字头语如“标准三角语言(Standard Triangle Language)”、“标准曲面细分语言(Standard Tessellation Language)”、“立体光刻语言(STereolithography Language)”和“(立体光刻曲面细分语言)”。许多套装软件支持这种格式,它被广泛用于快速成型、3D打印和计算机辅助制造(CAM)。STL文件仅描述三维物体的表面几何形状,没有颜色、材质贴图或其它常见三维模型的属性。

STL格式有文字和二进码两种型式。二进码型式因较简洁而较常见。

ASCII格式

ASCII码格式的STL文件逐行给出三角面片的几何信息,每一行以1个或2个关键字开头。

在STL文件中的三角面片的信息单元 facet 是一个带矢量方向的三角面片,STL三维模型就是由一系列这样的三角面片构成。整个STL文件的首行给出了文件路径及文件名。在一个 STL文件中,每一个facet由7 行数据组成,facet normal 是三角面片指向实体外部的法矢量坐标,outer loop 说明随后的3行数据分别是三角面片的3个顶点坐标,3顶点沿指向实体外部的法矢量方向逆时针排列。

ASCII格式的STL 文件结构如下:

//字符段意义

solidfilenamestl//文件路径及文件名

facetnormalxyz//三角面片法向量的3个分量值

outerloop

vertexxyz//三角面片第一个顶点坐标

vertexxyz//三角面片第二个顶点坐标

vertexxyz//三角面片第三个顶点坐标

endloop

endfacet//完成一个三角面片定义

…//其他facet

解析输出为:
C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019_第1张图片

二进制格式

二进制STL文件用固定的字节数来给出三角面片的几何信息。

文件起始的80个字节是文件头,用于存贮文件名;(如下所示)

solid Exported from Blender-2078 (sub 0)

紧接着用 4 个字节的整数来描述模型的三角面片个数,如下所示,即为一个三角得信息。

facet normal -0.000000 0.000000 -1.000000
outer loop
vertex 1.000000 1.000000 -1.000000
vertex 1.000000 -1.000000 -1.000000
vertex -1.000000 -1.000000 -1.000000
endloop
endfacet

后面逐个给出每个三角面片的几何信息。每个三角面片占用固定的50个字节,依次是:

3个4字节浮点数(角面片的法矢量)

3个4字节浮点数(1个顶点的坐标)

3个4字节浮点数(2个顶点的坐标)

3个4字节浮点数(3个顶点的坐标)个

三角面片的最后2个字节用来描述三角面片的属性信息。

一个完整二进制STL文件的大小为三角形面片数乘以 50再加上84个字节。

二进制格式的STL 文件结构如下:

UINT8//Header//文件头

UINT32//Numberoftriangles//三角面片数量

//foreachtriangle(每个三角面片中)

REAL32[3]//Normalvector//法线矢量

REAL32[3]//Vertex1//顶点1坐标

REAL32[3]//Vertex2//顶点2坐标

REAL32[3]//Vertex3//顶点3坐标

UINT16//Attributebytecountend//文件属性统计

解析输出为:
C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019_第2张图片

OBJ文件

obj(或者.obj)是一种几何定义文件格式,第一次是 Wavefront Technologies在他们的可视化加强动画包里面使用的。文件格式是公开的,并能很好的在其他的3D应用中被支持。

Obj文件格式是一种简单的单独表示3D几何图元的文件格式——也就是,顶点的坐标,每个顶点纹理的UV坐标,顶点法向量,以及组成多边形的面的顶点坐标、以及纹理UV坐标序列。面的顶点默认为逆时针顺序,法向量不是必须的。OBJ文件并非归一化的,但是可以在注释中加入缩放信息。

Obj文件可以是ASCII的编码(.obj)方式也可以是二进制格式(.mod)。但是二进制类型其作为专利未公开,因此这里不作讨论。以ASCII格式存储的obj文件必须用.obj作为文件拓展名。

文件的开始,有以哈希字符(#)开始的一行表示注释。

# this is a comment

对于obj来说,一个obj格式的文件可能包含了顶点数据,自由形式的曲面/表面属性,绘制索引序列,自由形式的曲面/表面内容声明,关联自由形式的表面,组和渲染属性信息。大多数常见的绘制索引表现为几何顶点,纹理坐标,顶点法线以及多边形的面:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gK3xEAGG-1590112131477)(https://raw.githubusercontent.com/adonispeace/adonispeace.github.io/master/dailyPic/20200401/clip_image012.jpg “Image_border+rounded”)]{:.border.rounded}

通常处理obj文件的时候,会抛弃顶点法线数据,而通过顶点信息来进行计算。有了以上的顶点坐标、法线、纹理坐标等信息,就可以进行3D模型文件的渲染了。

解析输出为:
C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019_第3张图片g “Image_border+rounded”)]{:.border.rounded}

3DS文件

3ds文件是3D Max的一种二进制存储格式,它始终没被官方公开,但是也基本被大家hack出来了大半。其“格式”总的来说非常简单,这里介绍一个概念:chunk。3ds文件里的数据都是按chunk一块一块隔离的。每个chunk都有两个标记:2个字节大小的chunkId,用来标识这个chunk存的是什么数据。接着是一个4个字节大小的chunkLen,它根据chunkId不同,可能表示该chunk的大小,也可能表示下一个chunk的位置偏移。

-----------------------------

chunk Id 2 Byte

chunk Len  4 Byte

-----------------------------

想要读取3ds文件,chunk Id是比较重要的部分,下面是hack出来的常用Id:

---------------------------------------------------------------------------------------

0x4D4D // Main Chunk

├─ 0x3D3D // 3D Editor Chunk

│ ├─ 0x4000 // Object Block

│ │ ├─ 0x4100 // Triangular Mesh

│ │ │ ├─ 0x4110 // Vertices List

│ │ │ ├─ 0x4120 // Faces Description

│ │ │ │ └─ 0x4130 // Faces Material

│ │ │ ├─ 0x4140 // Mapping Coordinates List

│ │ │ │ └─ 0x4150 // Smoothing Group List

│ │ │ └─ 0x4160 // Local Coordinates System

│ │ ├─ 0x4600 // Light

│ │ │ └─ 0x4610 // Spotlight

│ │ └─ 0x4700 // Camera

│ └─ 0xAFFF // Material Block

│ ├─ 0xA000 // Material Name

│ ├─ 0xA010 // Ambient Color

│ ├─ 0xA020 // Diffuse Color

│ ├─ 0xA030 // Specular Color

│ ├─ 0xA200 // Texture Map 1

│ ├─ 0xA230 // Bump Map

│ └─ 0xA220 // Reflection Map

│ │ /* Sub Chunks For Each Map */

│ ├─ 0xA300 // Mapping Filename

│ └─ 0xA351 // Mapping Parameters

└─ 0xB000 // Keyframer Chunk

├─ 0xB002 // Mesh Information Block

├─ 0xB007 // Spot Light Information Block

└─ 0xB008 // Frames (Start and End)

├─ 0xB010 // Object Name

├─ 0xB013 // Object Pivot Point

├─ 0xB020 // Position Track

├─ 0xB021 // Rotation Track

├─ 0xB022 // Scale Track

└─ 0xB030 // Hierarchy Position

---------------------------------------------------------------------------

一般来说,一个chunk可能还包含子chunk,比如0x4100表示Mesh的chunk里就包含了0x4110,0x4120等chunk,同时,0x4100这个chunk的长度标识,是把这些子chunk的长度也计算在内的。一般来说,我们读取3ds文件,只要解析主要chunk,遇到不识别的chunk,跳过即可。因为3ds里的数据,大多数并不是程序员需要的。我们只要顶点和uv这些就够了。

读取数据并载入:
C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019_第4张图片

配置环境

本人使用的VS2019,在配置环境时,希望能够讲库进行集中管理,因此在E盘单独建立文件夹,将include和lib项放入,并在项目中引入,虽然在后期新建项目中需要重复引入,但是保证了相关文件的整理便捷性。
C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019_第5张图片

同时添加依赖项:
在这里插入图片描述

绘制三维模型

对于三种文件格式来说,有很多的选择来进行重绘,在这里我选择使用glut的库来完成操作。

上边已经显示出数据的解析,并且得到绘制的图形。可以先使用如下函数进行绘制,设窗口的大小以及形式等信息。

glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGBA | GLUT_MULTISAMPLE);
glutInitWindowPosition(100, 100);
glutInitWindowSize(GL_WIN_WIDTH, GL_WIN_HEIGHT);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH); // | GLUT_STENCIL
glutInit(argc, argv);
GLUTwindow = glutCreateWindow("powerful_reader~");
glutReshapeFunc(glutResize);

最后的选择如上图所示。

Arcball实现控制

由于屏幕是二维的,无法直接表示旋转,可以通过辅助几何体来完成。想象在屏幕后面有一个球体,球体正好与这个屏幕相切,如下图的俯视图所示
C++方式实现stl、obj、3DS三种3D模型加载,并实现Arcball方法控制旋转缩放平移(采用glut库等文件,具体可在项目文件中实现),开发软件为VS2019_第6张图片

我们在求旋转轴之前需要将二维坐标转化为三维向量OP,如下图,T为屏幕上的二维坐标,P为T向屏幕后方发出射线与球体相交的点。这里做作OM水平面平行于屏幕,P到水平线的深度z需要先行求得。

为了运算处理方便,我们需要将T坐标限定在[-1,1]之间,设定这个球体的半径为1。

x = 2*x / 屏幕宽度 - 1

y = -(2*y / 屏幕高度 - 1)

由勾股定理可知,z = sqrt(1-xx-yy)。

当xx+yy大于1时,P不在球体上,那就在OM上找一点P,且满足P在球体上,PT垂直于平面OM,即xx+yy大小限定在1这个值,z = 0.

具体流程如下:

x,y,z的值可以唯一确定OP向量,接下来,给定输入的两个坐标T1,T2,我们做出如下处理:

\1. 分别求出T1和T2对应的三维向量OP1和OP2

\2. s = OP1 · OP2,即先求两个向量的内积

\3. v = OP1 × OP2,即求两个向量的外积

\4. 记四元数q = [s, v],将其单位化,此时q为旋转四元数。

\5. 旋转角α = 2arccos(q.s) ,旋转轴V = ( q.v / sqrt(1-q.s*q.s) )

if (rotating) {
			//控制旋转的信号
			rotation[0] += -0.5 * (y - GLUTmouse[1]);
			rotation[2] += 0.5 * (x - GLUTmouse[0]);
			//rotation[1] += 0.5 * (x - GLUTmouse[0]);
			//这样可以面向你进行旋转
			//在重绘的时候就可以了
		}
		else if (scaling) {
			// 控制缩放信号
			//GLUTmouse存储了之前记录的点信息
			scale *= exp(2.0 * (float)((x - GLUTmouse[0])) / (float)GL_WIN_WIDTH);
			//如果想调成按照y方向控制可以:
			// scale *= exp(2.0 * (float)( (y- GLUTmouse[1])) / (float) GL_WIN_WIDTH);
		}
		else if (translating) {
			// 控制平移信号
			translation[0] += 2.0 * (float)(x - GLUTmouse[0]) / (float)GL_WIN_WIDTH;
			translation[1] += 2.0 * (float)(y - GLUTmouse[1]) / (float)GL_WIN_HEIGHT;
		}
		//我们在拖拽旋转的过程中需要设置定点中心
		GLUTmouse[0] = x;
		GLUTmouse[1] = y;

如下边所示,在main.cpp代码中定义glutMotion()函数,对各个数据进行操作,最后完成对三维体的平移、旋转以及缩放信号。

之后通过glut库函数对控制变量进行输入:

glLoadIdentity();
		glScalef(scale, scale, scale);
		glTranslatef(translation[0], translation[1], 0.0);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		gluPerspective(45.0, (GLfloat)GL_WIN_WIDTH / (GLfloat)GL_WIN_HEIGHT, 0.1, 100.0);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glTranslatef(translation[0], translation[1], translation[2]);
		glScalef(scale, scale, scale);
		//刷新放缩的大小
		glRotatef(rotation[0], 1.0, 0.0, 0.0);
		glRotatef(rotation[1], 0.0, 1.0, 0.0);          //控制不同角度
		glRotatef(rotation[2], 0.0, 0.0, 1.0);
		glTranslatef(-center[0], -center[1], -center[2]);
		//改变旋转中心
		glClearColor(1.0, 1.0, 1.0, 1.0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		//设置光照
		//载入不同光源的位置
		glLightfv(GL_LIGHT0, GL_POSITION, light0_position);
		glLightfv(GL_LIGHT1, GL_POSITION, light1_position);
		//定义材料信息
		//这里可以调整环境颜色和散射颜色数组
		glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, my_set_material);
		// 下面开始绘制表面
		if (if_face == 1)
			draw_faces();
		if (if_line == 1)
			draw_lines();
		if (if_point == 1)
			draw_points();
		glutSwapBuffers();

并且使用glut的库函数,设置鼠标以及键盘事件,完成操作:

	glutKeyboardFunc(glutKeyboard);
	glutMouseFunc(glutMouse);
	glutMotionFunc(glutMotion);

同时,对光纤等材质信息进行绘制。

glutSpecialFunc(GLUTSpecial);
		glutIdleFunc(0);
		// 设置光照信息
		static GLfloat lmodel_ambient[] = { 0.2, 0.2, 0.2, 1.0 };
		glLightModelfv(GL_LIGHT_MODEL_AMBIENT, lmodel_ambient);
		glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
		static GLfloat light0_diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
		//设置满散射
		glLightfv(GL_LIGHT0, GL_DIFFUSE, light0_diffuse);
		glEnable(GL_LIGHT0);
		static GLfloat light1_diffuse[] = { 0.5, 0.5, 0.5, 1.0 };
		glLightfv(GL_LIGHT1, GL_DIFFUSE, light1_diffuse);
		glEnable(GL_LIGHT1);
		glEnable(GL_NORMALIZE);
		glEnable(GL_LIGHTING);
		glEnable(GL_DEPTH_TEST);

在主函数中,可以通过打开文件的方式获取文件的路径,并且通过判断文件路径的“点符号”确定文件类型,然后调用相关的函数,最终完成绘制。

OPENFILENAME ofn;      // 公共对话框结构。     
	TCHAR szFile[MAX_PATH]; // 保存获取文件名称的缓冲区。               
	// 初始化选择文件对话框。     
	ZeroMemory(&ofn, sizeof(OPENFILENAME));
	ofn.lStructSize = sizeof(OPENFILENAME);
	ofn.hwndOwner = NULL;
	ofn.lpstrFile = szFile;
	ofn.lpstrFile[0] = '\0';
	ofn.nMaxFile = sizeof(szFile);
	ofn.lpstrFilter = (LPCSTR)"all(*.*)\0 * .*\0";
	ofn.nFilterIndex = 1;
	ofn.lpstrFileTitle = NULL;
	ofn.nMaxFileTitle = 0;
	ofn.lpstrInitialDir = NULL;
	ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
	//ofn.lpTemplateName =  MAKEINTRESOURCE(ID_TEMP_DIALOG);    
	// 显示打开选择文件对话框。
	if (GetOpenFileName(&ofn))
	{
		//显示选择的文件。 
		std::cout << szFile << std::endl;
		OutputDebugString(szFile);    //这一句是显示路径吗?为什么不显示?
		OutputDebugString((LPCSTR)"\r\n");
	}
	TCHAR* file_name;
	file_name = szFile;
	string filename = file_name;
	string suffixStr = filename.substr(filename.find_last_of('.') + 1);
	cout << "houzui shi " << suffixStr << endl;

结果展示

如下所示,stl格式的ascii、binary存储格式的读取和绘制以及控制,以及obj和3DS格式,运行过程存储为gif****格式,可另行保存观看。

或者在百度云下载:

链接:https://pan.baidu.com/s/1mH9cy1nC9WNrOfa7HDqa9g

提取码:xv8t

STL_binary

STL_ASCII

OBJ

3DS

本文提到的代码已上传,欢迎下载:
https://download.csdn.net/download/qq_32563773/12445747

你可能感兴趣的:(虚拟现实,几何学,visual,studio,c++)