OpenGL 繪製你的 github skyline 模型

ITryagain發表於2021-03-25

前言

好久沒更新部落格了,上一篇文章還是在去年6月份,乍一看居然快要過去一年了,不時還能看到粉絲數和排名在漲,可是卻一直沒有內容更新,怪不好意思的- -(主要是一直沒想好要寫些什麼,中間也有過一些想法,比如從 Python 的 yield 關鍵字講到協程、基於蒙特卡洛方法的 Raytracing、ECS等等,但是總覺得不是能夠講的很好就一直沒寫,然後平時不是在打 Dota2 就是在趕專案,所以就一直咕咕咕了。。。)

回到正題,前段時間看到的 github skyline,就是將過去一年在 github 的 commit 視覺化成一個三維的模型,如下圖。

當時看到這個第一反應就是好炫!然後看到了 Download STL file 這個按鈕,又想到目前正在設計的並將作為學校圖形學實驗使用的專案(專案地址:https://github.com/leo6033/disc0ver-Engine ),就萌生了將繪製這個 model 做為模型繪製實驗的想法。於是便開始了動手實現。(關於 OpenGL 模型載入的文章,我在兩年前的從零開始 OpenGL 系列中倒是有寫過,不過當時的實現是基於固定渲染管線的,從去年開始接觸了 OpenGL 可程式設計流水線,就打算後面進行逐步更新)

模型繪製

STL 模型

將 STL file 下載下來之後,看到字尾名 stl,頭一次見到這種字尾名,開啟檔案看了看

第一反應,這個格式有那麼點點奇怪啊,字串好像有那麼點多。。。不過仔細分析了一下,大概就是這樣

solid stlmesh
facet normal n1 n2 n3
	outer loop
		vertex x1 y1 z1
		vertex x2 y2 z2
		vertex x3 y3 z3
	endloop
end facet
...
...
facet normal n1 n2 n3
	outer loop
		vertex x1 y1 z1
		vertex x2 y2 z2
		vertex x3 y3 z3
	endloop
end facet
endsolid stlmesh

那麼,用一個大迴圈即可解決,虛擬碼如下

s = readline()
assert(s == "solid filename")
while(!eof){
	normal = readline()
	if "facet" in normal{
        normal -> (n1, n2, n3)
        readline() skip outer loop
        for i in range(3){
            vertex = readline()
            vertex -> (x, y, z)
        }
        readline() skip end loop
    }
}
程式碼實現
#include <sstream>
#include <fstream>
#include <iostream>
void disc0ver::Model::loadModel(const std::string path)
{
	std::cout << "load model " << path << std::endl;
	std::ifstream infile;
	std::string tmp_str;
	infile.open(path);

	if(!infile.is_open())
	{
		throw "model not found";
	}
	
	// 讀取頭 solid filename
	char line[256];
	infile.getline(line, sizeof(line));
	std::istringstream solid(line);
	solid >> tmp_str;
	assert(tmp_str == "solid");
	
	while(!infile.eof())
	{
		infile.getline(line, sizeof(line));
		float n1, n2, n3;
		float x, y, z;
		std::istringstream face(line);
	
		face >> tmp_str;
		if(tmp_str == "facet")
		{
			face >> tmp_str >> n1 >> n2 >> n3;
			// outer loop
			infile.getline(line, sizeof(line));
			for(int i = 0; i < 3;i ++)
			{
				infile.getline(line, sizeof(line));
				std::istringstream vertex(line);
				vertex >> tmp_str >> x >> y >> z;
				vertices.emplace_back(x, y, z, n1, n2, n3, 0.0f, 0.0f);
			}
			// end loop
			infile.getline(line, sizeof(line));
		}
		// end facet
		infile.getline(line, sizeof(line));
	}

}

模型繪製

在專案中,我封裝了個 Mesh 類,將 OpenGL 圖形繪製都放置於 Mesh 中進行

void disc0ver::Mesh::Draw(Shader &shader)
{
	for(unsigned int i = 0; i < textures.size(); i ++)
	{
        textures[i].use(i);
        shader.setInt(textures[i].getName(), static_cast<int>(i));
	}
	
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
    glBindVertexArray(0);
	
    glActiveTexture(GL_TEXTURE0);
}

void disc0ver::Mesh::setupMesh()
{
    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), static_cast<void*>(0));
    // 頂點法線
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(offsetof(Vertex, normal)));
    // 頂點紋理座標
    glEnableVertexAttribArray(2);
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), reinterpret_cast<void*>(offsetof(Vertex, texCoords)));

    glBindVertexArray(0);
}

因此,我們需要做的就是如模型讀取程式碼那樣將讀取到的 normal 和 vertex 存至 vertices 陣列中,然後在 Init 函式中建立 indices 陣列,作為頂點索引,最後直接呼叫 mesh.Draw() 來實現模型的繪製。

void disc0ver::Model::Init()
{
	std::vector<Texture> tmp;
	for (auto it = textures.begin(); it != textures.end(); ++it)
	{
		tmp.push_back(it->second);
	}
	for (int i = 0; i < vertices.size(); i++)
	{
		indices.push_back(i);
	}
	Mesh mesh(vertices, indices, tmp);
	meshes.push_back(mesh);
}

void disc0ver::Model::draw(Shader& shader)
{
	transform.use();
	for (auto& mesh : meshes)
	{
		mesh.Draw(shader);
	}
}

效果展示

加入了簡單的光線反射 shader 效果

小節

由於很多 OpenGL 的繪製介面在專案中被封裝起來了,所以給的程式碼片段看起來會比較迷糊,感覺比較單獨看比較好理解的也就是模型資料讀取的那部分程式碼了,歡迎大家閱讀專案的原始碼,並希望能給些建議,如果可以點個 star 的話就更好了 _ (:з」∠)_ 。後續將繼續更新一些專案內容的實現原理,以及去年做的基於蒙特卡洛方法的 Raytracing。

本文首發於我的知乎

相關文章