【OpenGL】CPP读取STL文件并通过OpenGL显示

在这里插入图片描述

1. 引言

在学习OpenGL的过程中,有很多同学都卡在了导入模型这一步。由于assimp库的编译和配置比较复杂,如果使用官方编译好的库会则不具有良好的跨平台覆盖;而如果自己进行编译,有可能会在进行CMAKE编译的时候出现类似于‘DirectX库缺失’这样的错误。此外,虽然assimp库支持几十种模型文件的读取,但对于初学者来说,这并不是必须的。为了在一些特殊场景下读取并显示STL文件,本文尝试采用相对简单的语法手写STL文件的读取,便于大家认识STL文件及其结构,并通过OpenGL将所读取的模型显示出来。


2. 认识STL

STL文件是一种以记录三角形面片来描述立体模型的文件结构,通常作为CAD以及3D打印的常用交换格式。常见的STL文件主要有二进制和ASCII两种记录方式。在Inventor等软件中,导出模型时可以自行选择导出格式。
在这里插入图片描述

ASCII格式的STL文件非常易读,一个立方体的文件如下。

solid ASCII // 表示文件采用ASCII存储
  facet normal -1.000000e+00 0.000000e+00 0.000000e+00 //面的法向量,指向立方体的外部,可以通过右手定则恢复顶点顺序
    outer loop
      vertex   -5.000000e+00 0.000000e+00 -5.000000e+00//顶点的x,y,z座标
      vertex   -5.000000e+00 0.000000e+00 5.000000e+00
      vertex   -5.000000e+00 1.000000e+01 -5.000000e+00//三个顶点构成三角形
    endloop
endfacet
//……省略其余7个顶点……
endsolid//文件结束

值得注意的是,顶点座标的单位是由生成STL文件的软件决定的,但并不会体现在stl文件中。例如上面数据的单位是毫米,在我们导入时需要注意单位可能需要转换。

二进制的STL Binary文件结构也很清晰。因其冗余内容较少,大多数软件默认导出的STL文件都是二进制格式的。

UINT8//Header//文件头,共80字节,存贮文件名;
UINT32//Numberoftriangles//4个字节的整数描述三角面片数量
//foreachtriangle(每个三角面片中占用50字节)
REAL32[3] //Normalvector//法线矢量,3个4字节浮点数
REAL32[3] //Vertex1//顶点1座标,3个4字节浮点数
REAL32[3] //Vertex2//顶点2座标,3个4字节浮点数
REAL32[3] //Vertex3//顶点3座标,3个4字节浮点数
UINT16//Attributebytecountend//二个字节,文件属性统计

本文优先关注方便读取的ASCII格式的STL文件,二进制文件则留待以后再研究。


3. STL文件读取

本文为了简化读入的逻辑,对于STL文件的读取没有进行任何的读入优化,用一个word字符串来读取所有的英文单词。
代码如下:

float pow0_1(int a)
{
    float pow = 1.0;
    for(;a>=0;a--)
        pow *= 0.1;
    return pow;
}
float pow10(int a)
{
    float pow = 1.0;
    for(;a>=0;a--)
        pow *= 10.0;
    return pow;
}
float gen_vertex(char input[15])
{
    int temp1,temp2;float v;float multipler=0.0000001;
    if(input[0]=='-')
    {
        temp1=(input[1]-48)*1000000+(input[3]-48)*100000+(input[4]-48)*10000+(input[5]-48)*1000+(input[6]-48)*100+(input[7]-48)*10+(input[8]-48)*1;
        temp2=(input[11]-48)*10+(input[12]-48);
        if(input[10]=='-')
            multipler *= pow0_1(temp2);
        else
            multipler *= pow10(temp2);
        v = (float)temp1 * multipler * (-1.0);
    }
    else
    {
        temp1=(input[0]-48)*1000000+(input[2]-48)*100000+(input[3]-48)*10000+(input[4]-48)*1000+(input[5]-48)*100+(input[6]-48)*10+(input[7]-48)*1;
        temp2=(input[10]-48)*10+(input[11]-48);
        if(input[9]=='-')
            multipler *= pow0_1(temp2);
        else
            multipler *= pow10(temp2);
        v = (float)temp1 * multipler;
    }
    return v;
}/////////////////////////////这三个函数返回顶点某个分量的浮点数值
void STL_Read()
{
	char word[15];
    freopen("2_ASCII.stl","r",stdin);
    cin >> word;cin >> word;//solid ascii
    int i;
    char vertex[15];
    for(i=1;;i++)
    {
        cin >> word;
        if(word[0]=='e')
        {
            break;
        }//如果读入的是endsolid,则结束顶点输入
        cin >> word;cin >> word;cin >> word;cin >> word;/////////facet normal
        cin >> word;cin >> word;/////////outer loop
        ///////////////////读入第一个顶点
        cin >> word;
        cin >> vertex;vertices[VerticesCnt] = gen_vertex(vertex) * 0.1;
		cin >> vertex;vertices[VerticesCnt+1] = gen_vertex(vertex)*0.1;
		cin >> vertex;vertices[VerticesCnt+2] = gen_vertex(vertex)*0.1;
		vertices[VerticesCnt+3] = 0.3f;
		vertices[VerticesCnt+4] = 0.4f;
		vertices[VerticesCnt+5] = 0.5f;//设置顶点颜色
		VerticesCnt += 6;
		////////////////////////////////读入第二个顶点
		cin >> word;
        cin >> vertex;vertices[VerticesCnt] = gen_vertex(vertex) * 0.1;
		cin >> vertex;vertices[VerticesCnt+1] = gen_vertex(vertex)*0.1;
		cin >> vertex;vertices[VerticesCnt+2] = gen_vertex(vertex)*0.1;
		vertices[VerticesCnt+3] = 0.5f;
		vertices[VerticesCnt+4] = 0.5f;
		vertices[VerticesCnt+5] = 1.0f;//设置顶点颜色
		VerticesCnt += 6;
		//////////////////////////////////////////////读入第三个顶点
		cin >> word;
        cin >> vertex;vertices[VerticesCnt] = gen_vertex(vertex) * 0.1;
		cin >> vertex;vertices[VerticesCnt+1] = gen_vertex(vertex)*0.1;
		cin >> vertex;vertices[VerticesCnt+2] = gen_vertex(vertex)*0.1;
		vertices[VerticesCnt+3] = 0.5f;
		vertices[VerticesCnt+4] = 0.4f;
		vertices[VerticesCnt+5] = 0.0f;//设置顶点颜色
		VerticesCnt += 6;
		////////////////////////////////////
		cin >> word;//endloop
		cin >> word;//endfacet

    }
    fclose(stdin);
}


然后,在main函数里运行STL_Read函数,就可以得到STL的顶点数据,再调用OpenGL显示就十分方便了。


4. 代码全文

总体流程是:

  1. 初始化并创建窗口
  2. 加载立方体顶点VAOVBO以及着色器并开启深度测试、
  3. 进入主循环清除缓冲
  4. 使用立方体着色器,构造并传入pvm矩阵,绘制
  5. 循环结束,释放VAOVBO

关于OpenGL的部分可以配合注释自行理解,之后我也会尝试更新关于OpenGL的内容。

#include <iostream>
#include <stdio.h>
#include <windows.h>
#include <math.h>
#include <vector>
#include "glad/glad.h"
#include "GLFW/glfw3.h"
#include "glm/glm.hpp"
#include "glm/gtc/matrix_transform.hpp"
#include "glm/gtc/type_ptr.hpp"
#include "shader.h"
using namespace std;
float vertices[1000000];//顶点数组
float screen_width = 1440.0f;          //窗口宽度
float screen_height = 960.0f;          //窗口高度
//鼠标键盘响应函数
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void mouse_callback(GLFWwindow* window, double xpos, double ypos);
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
void processInput(GLFWwindow *window);

//相机参数
glm::vec3 camera_position = glm::vec3(0.0f, 0.0f, 3.0f);     //摄像机位置
glm::vec3 camera_front = glm::vec3(0.0f, 0.0f, -1.0f);       //摄像机方向
glm::vec3 camera_up = glm::vec3(0.0f, 1.0f, 0.0f);           //摄像机上向量
glm::vec3 camera_right = glm::cross(camera_front,camera_up);
//视野
float fov = 45.0f;
float deltaTime = 0.0f;
float lastFrame = 0.0f;
float currentFrame = 0.0f;
int fpscnt=0;
float fpsTime = 0.0f;
int VerticesCnt = 0;
///////////////////////////////////////////////////////////////////////
float pow0_1(int a)
{
    float pow = 1.0;
    for(;a>=0;a--)
        pow *= 0.1;
    return pow;
}
float pow10(int a)
{
    float pow = 1.0;
    for(;a>=0;a--)
        pow *= 10.0;
    return pow;
}
float gen_vertex(char input[15])
{
    int temp1,temp2;float v;float multipler=0.0000001;
    if(input[0]=='-')
    {
        temp1=(input[1]-48)*1000000+(input[3]-48)*100000+(input[4]-48)*10000+(input[5]-48)*1000+(input[6]-48)*100+(input[7]-48)*10+(input[8]-48)*1;
        temp2=(input[11]-48)*10+(input[12]-48);
        if(input[10]=='-')
            multipler *= pow0_1(temp2);
        else
            multipler *= pow10(temp2);
        v = (float)temp1 * multipler * (-1.0);
    }
    else
    {
        temp1=(input[0]-48)*1000000+(input[2]-48)*100000+(input[3]-48)*10000+(input[4]-48)*1000+(input[5]-48)*100+(input[6]-48)*10+(input[7]-48)*1;
        temp2=(input[10]-48)*10+(input[11]-48);
        if(input[9]=='-')
            multipler *= pow0_1(temp2);
        else
            multipler *= pow10(temp2);
        v = (float)temp1 * multipler;
    }
    return v;
}
///////////////////////////////////////////////////////////////////
void STL_Read()
{
	char word[15];
    freopen("2_ASCII.stl","r",stdin);
    cin >> word;cin >> word;//solid ascii
    int i;
    char vertex[15];
    for(i=1;;i++)
    {
        cin >> word;
        if(word[0]=='e')
        {
            break;
        }
        cin >> word;cin >> word;cin >> word;cin >> word;/////////facet normal
        cin >> word;cin >> word;/////////outer loop
        ///////////////////
        cin >> word;
        cin >> vertex;vertices[VerticesCnt] = gen_vertex(vertex) * 0.1;
		cin >> vertex;vertices[VerticesCnt+1] = gen_vertex(vertex)*0.1;
		cin >> vertex;vertices[VerticesCnt+2] = gen_vertex(vertex)*0.1;
		vertices[VerticesCnt+3] = 0.3f;
		vertices[VerticesCnt+4] = 0.4f;
		vertices[VerticesCnt+5] = 0.5f;
		VerticesCnt += 6;
		////////////////////////////////
		cin >> word;
        cin >> vertex;vertices[VerticesCnt] = gen_vertex(vertex) * 0.1;
		cin >> vertex;vertices[VerticesCnt+1] = gen_vertex(vertex)*0.1;
		cin >> vertex;vertices[VerticesCnt+2] = gen_vertex(vertex)*0.1;
		vertices[VerticesCnt+3] = 0.5f;
		vertices[VerticesCnt+4] = 0.5f;
		vertices[VerticesCnt+5] = 1.0f;
		VerticesCnt += 6;
		//////////////////////////////////////////////
		cin >> word;
        cin >> vertex;vertices[VerticesCnt] = gen_vertex(vertex) * 0.1;
		cin >> vertex;vertices[VerticesCnt+1] = gen_vertex(vertex)*0.1;
		cin >> vertex;vertices[VerticesCnt+2] = gen_vertex(vertex)*0.1;
		vertices[VerticesCnt+3] = 0.5f;
		vertices[VerticesCnt+4] = 0.4f;
		vertices[VerticesCnt+5] = 0.0f;
		VerticesCnt += 6;
		////////////////////////////////////
		cin >> word;//endloop
		cin >> word;//endfacet
	}
    fclose(stdin);
}
int main() {
	// 初始化GLFW
	STL_Read();
	printf("Vertices = %d\n",VerticesCnt);
	for(int qiuzili = 0;qiuzili<=VerticesCnt;qiuzili++)
	{
		printf("%.6f\n",vertices[qiuzili]);
	}
	glfwInit();                                                     // 初始化GLFW
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);                  // OpenGL版本为3.3,主次版本号均设为3
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 使用核心模式(无需向后兼容性)
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 如果使用的是Mac OS X系统,需加上这行
	glfwWindowHint(GLFW_RESIZABLE, 0);						    // 不可改变窗口大小

																	// 创建窗口(宽、高、窗口名称)
	auto window = glfwCreateWindow(screen_width, screen_height, "Cube", nullptr, nullptr);
	if (window == nullptr) {                                        // 如果窗口创建失败,输出Failed to Create OpenGL Context
		std::cout << "Failed to Create OpenGL Context" << std::endl;
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);                                 // 将窗口的上下文设置为当前线程的主上下文
																	// 初始化GLAD,加载OpenGL函数指针地址的函数
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
	{
		std::cout << "Failed to initialize GLAD" << std::endl;
		return -1;
	}
	// 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
	glViewport(0, 0, screen_width, screen_height);
	Shader shader("res/shader/task-cube.vs", "res/shader/task-cube.fs");//加载着色器
	// 生成并绑定VAO和VBO
	GLuint vertex_array_object; // == VAO
	glGenVertexArrays(1, &vertex_array_object);
	glBindVertexArray(vertex_array_object);
	GLuint vertex_buffer_object; // == VBO
	glGenBuffers(1, &vertex_buffer_object);
	glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);
	// 将顶点数据绑定至当前默认的缓冲中
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	// 设置顶点属性指针
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
	glEnableVertexAttribArray(1);
	glEnable(GL_DEPTH_TEST);
	// Render loop主循环
	while (!glfwWindowShouldClose(window)) {
		//计算每帧的时间差
		currentFrame = (float)glfwGetTime();
		deltaTime = currentFrame - lastFrame;
		lastFrame = currentFrame;
		++fpscnt;
		if(currentFrame - fpsTime >= 2.0)
		{
			printf("fps=%d     fov=%.2f    ",fpscnt/2,fov);
			printf("pos= %.2f %.2f %.2f\n",camera_position[0],camera_position[1],camera_position[2]);
			fpscnt = 0;
			fpsTime = currentFrame;
		}
		processInput(window);
		//进入主循环,清理颜色缓冲深度缓冲
		glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);//清理颜色缓冲和深度缓冲
		shader.Use();
		// Transform座标变换矩阵
		glm::mat4 model(1);//model矩阵,局部座标变换至世界座标
		model = glm::translate(model, glm::vec3(0.0,0.0,0.0));
		model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.5f, 1.0f, 0.0f));
		model = glm::scale(model, glm::vec3(1.0f,1.0f,1.0f));
		glm::mat4 view(1);//view矩阵,世界座标变换至观察座标系
		view = glm::lookAt(camera_position, camera_position + camera_front, camera_up);
		glm::mat4 projection(1);//projection矩阵,投影矩阵
		projection = glm::perspective(glm::radians(fov), (float)screen_width / screen_height, 0.1f, 100.0f);
		// 向着色器中传入参数
		int model_location = glGetUniformLocation(shader.ID, "model"); //获取着色器内某个参数的位置
		glUniformMatrix4fv(model_location, 1, GL_FALSE, glm::value_ptr(model));//写入参数值
		int view_location = glGetUniformLocation(shader.ID, "view");
		glUniformMatrix4fv(view_location, 1, GL_FALSE, glm::value_ptr(view));
		int projection_location = glGetUniformLocation(shader.ID, "projection");
		glUniformMatrix4fv(projection_location, 1, GL_FALSE, glm::value_ptr(projection));
		//绘制
		glBindVertexArray(vertex_array_object);
		glDrawArrays(GL_TRIANGLES, 0, VerticesCnt / 6);
		glBindVertexArray(0);
		
		glfwSwapBuffers(window);
		glfwPollEvents();
	}
	//释放VAOVBO
	glDeleteVertexArrays(1, &vertex_array_object);
	glDeleteBuffers(1, &vertex_buffer_object);

	// 清理所有的资源并正确退出程序
	glfwTerminate();
	return 0;
}
void processInput(GLFWwindow *window)
{
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
		glfwSetWindowShouldClose(window, true);

	if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS)
		camera_position += camera_front * deltaTime ;
	if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS)
		camera_position -= camera_front * deltaTime ;
	if (glfwGetKey(window, GLFW_KEY_A) == GLFW_PRESS)
		camera_position -= camera_right * deltaTime;
	if (glfwGetKey(window, GLFW_KEY_D) == GLFW_PRESS)
		camera_position += camera_right * deltaTime;
	
	if (glfwGetKey(window, GLFW_KEY_UP) == GLFW_PRESS)
		camera_front += camera_up * deltaTime;	
	if (glfwGetKey(window, GLFW_KEY_DOWN) == GLFW_PRESS)
		camera_front -= camera_up * deltaTime;	
	if (glfwGetKey(window, GLFW_KEY_LEFT) == GLFW_PRESS)
		camera_front -= camera_right * deltaTime;
	if (glfwGetKey(window, GLFW_KEY_RIGHT) == GLFW_PRESS)
		camera_front += camera_right * deltaTime;

	if (glfwGetKey(window, GLFW_KEY_Z) == GLFW_PRESS)
	{
		fov=fov+10.0*deltaTime;
		if(fov>179.0)
			fov=179.0;
		//printf("%.5f %.2f\n",deltaTime,fov);
	}
	if (glfwGetKey(window, GLFW_KEY_X) == GLFW_PRESS)
	{
		fov=fov-10.0*deltaTime;
		if(fov<1.0)
			fov=1.0;
		//printf("%.5f %.2f\n",deltaTime,fov);
	}
}

运行效果:

觉得有用的话,不要吝惜评论点赞分享哦,希望大家多多包涵,有任何问题欢迎指正、讨论。
本文基于CC-BY-SA 4.0协议,欢迎转载
(博客看累了?去我的B站瞧一瞧?)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章