2015年11月12日 星期四

GLSL & GLUT with C++入門篇 - 01


這系列修改自我曾經發佈在電腦遊戲製作開發設計論壇的文章《GLSL&GLUT 從環境設定開始的基礎教學》

當初寫那份教學至今已經過了兩年,我也讀了不少新東西,雖然裏頭的內容仍然可以用,但稍微也想更新一下裡面的資訊,就趁著機會順便玩玩blog和寫點HTML。

我最近才知道google的blogger可以直接寫HTML和javascript,作為一個(自認)喜歡寫程式的人,真是無顏面對我自己(つд⊂)

那麼一樣做點前情提要:
1. 這系列教學適用條件:具備C++基礎知識、能夠利用GLUT繪製簡易圖形、熟悉二維三維數學(國高中程度)、少許線性代數觀念。
2. 雖然我使用的IDE是Visual Studio 2012,專案上我會盡量不使用IDE提供的管理,讓各個不同的IDE都能成功編譯。
3. 如果你是跟我一樣用Visual Studio來編譯程式,最後出來的.exe執行檔若要拿到別台電腦上執行,需確認該台電腦有安裝跟你VS一樣版本的「可轉散發套件」。
4. 對於視窗事件管理(滑鼠、鍵盤之類的),建議使用個人習慣的函式庫(Win32API、SDL之類的),在入門篇我都會使用GLUT的Callback函數。

再來是開始前的準備:
1. GLEW(GL Easy Extension library),點前面的超連結下載,這是要寫GLSL必備的函式庫,之後要實作的內容都需要他。
2. GLUT(The OpenGL Utility Toolkit),點前面的超連結下載,這是入門篇要用來當作視窗控制用的工具,因操作上容易,沒要寫大型程式的話這個比較方便。
※備點:GLEWGLUT
3. 函式庫的安裝不多介紹了,.h檔、.lib檔丟到IDE能抓到的地方(專案資料夾或以VS來說是在C:\Program Files (x86)\Microsoft Visual Studio 版本.0\VC\include還有lib),.dll放到專案資料夾裡(之後要跟著.exe執行檔跑)。

那麼環境準備好了,就先簡單介紹一下GLSL,他是OpenGL Shading Language的簡寫,我們要設計被稱作shader(著色器)的東西,簡言之就是去更直接的控制OpenGL對像素上色的語言,算是比較深入的圖學程式設計。

曾經單用GLUT無法做出的效果,如normal mapping(法線貼圖)就需要利用自製的fragment shader才有辦法實作:

GLSL在一支程式裡的立場就是,額外設計程式去讓你的顯示卡跑,詳細的vertex shader和fragment shader(DirectX裡叫做pixel shader),往後再慢慢介紹,這裡就先從第一個實作開始。

首先思考的是,如何在一支程式裡面再運作別的程式?

其實底層的東西都已經被建置完了,我們只需要寫好程式碼,存成字串丟給OpenGL內的編譯器就行,許多教學都會把shader的程式碼存成別的文件,這邊也不例外。

流程如下:
Step1: 把著色器的程式碼寫在兩個檔案(分vertex和fragment)
Step2: 讀取兩個文件把它存進memory(char陣列)
Step3: 經過官方文件內標示的特定流程compile他們
Step4: 然後就可以開始用你剛剛寫的著色器程式來渲染你的面


首先會需要用到的前置有這些
#include <string>
#include <iostream>
#include <fstream>

#include <GL\glew.h>	// 依個人路徑include
#include <GL\glut.h>

#pragma comment(lib, "glew32.lib")
#pragma comment(lib, "glut32.lib")


那這就開始正題:

Step1


shader分為兩個部分,vertex還有fragment,簡單來說vertex shader負責輸出頂點座標,fragment shader負責輸出像素顏色,詳細之後會慢慢講,這裡先簡單記憶下就行。(或者可到NeHe的網站,裡面有較為詳盡的解說,連結點我

這邊寫個最簡單的vertex shader和fragment shader(檔名隨意,跟讀寫檔相同即可)。
※先寫最舊版的glsl,下一章改作4.0,可與本章相比較
// vertex.vs 

void main() 
{ 
	gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}
// fragment.frag

void main()
{
	gl_FragColor  = vec4(1.0, 0.0, 0.0, 1.0);
}
gl_Position
-> 是一個4維的向量(vector),就是整個環境你設置的每一個vertex的最終型態,他只能在vertex shader中存在。
gl_FragColor
-> 是一個4維的向量,就是你畫面中每一個pixel的最終型態,他只能出現在fragment shader。

gl_ModelViewProjectionMatrix
-> 是一個4*4的矩陣,代表把vertex轉換成model-view-projection position的矩陣(即立體空間的座標轉變成螢幕上的平面2D座標),他的值來自我們使用的glMatrixMode以及其他轉換矩陣函式。
gl_Vertex
-> 是一個4維的向量,代表每一個vertex,他的值來自我們使用的glVertex系列函式。

※稱vector是向量可能不太好,畢竟不是全部的使用都會具有方向性,我也不能很好的說明,但我個人會認為把它看作1*4的矩陣可能會比較好理解。

這些在上面PO的那篇NeHe介紹GLSL的文章裡都有說到。

簡言之,無論OpenGL之後會對gl_Position和gl_FragColor做什麼,在最後算出來的gl_Position就會是你在畫面上面看到的每一個頂點。

而gl_FragColor就會是畫面上面每一個"pixel"的顏色,請注意不是每個頂點,是每個像素的顏色(這個和計算機圖學裡面的Graphic Rendering Pipeline裡的Rasterization有關,我這裡就不贅述了,有興趣可自行google)。

綜上所述,要理解那兩個子程式應該足夠簡單了。
vertex.vs裡面說的就是,我要把我的每個立體空間座標轉換成投影在螢幕上的點。
fragment.frag則是,把存在面的地方上的每個pixel都塗成紅色的。


那麼再來的問題就是怎麼compile這兩個shader程式碼了。

Step2


首先我們需要一個能夠讀取文件的Function。
void loadFile(const char* filename, std::string &string) 
{ 
	std::ifstream fp(filename); 
	if(!fp.is_open()){ 
		std::cout << "Open <" << filename << "> error." << std::endl; 
		return; 
	} 

	char temp[300]; 
	while(!fp.eof()){ 
		fp.getline(temp, 300); 
		string += temp; 
		string += '\n'; 
	} 

	fp.close(); 
}

很基礎的C++讀寫檔應該不用多介紹,就是把檔案存進引數的string裡。

然後就要把這段引進的程式碼跟OpenGL的關聯函式做連結了!

Step3-1

unsigned int loadShader(std::string &source, GLenum type) 
{ 
	// 告訴OpenGL我們要創的是哪種shader 
	unsigned int ShaderID; 
	ShaderID = glCreateShader(type);
	// 把std::string結構轉換成const char* 
	const char* csource = source.c_str();
	// 把程式碼放進去剛剛創建的shader object中 
	glShaderSource(ShaderID, 1, &csource, NULL);
	// 編譯shader 
	glCompileShader(ShaderID);
	// 這是編譯過程的訊息, 錯誤什麼的把他丟到error裡面 
	char error[1000] = ""; 
	glGetShaderInfoLog(ShaderID, 1000, NULL, error);
	// 然後輸出出來 
	std::cout << "Complie status: \n" << error << std::endl;

	return ShaderID; 
}
glCreateShader(type)
首先我們要告訴OpenGL我們現在要compile的是vertex shader還是fragment shader。
type通常是放GL_VERTEX_SHADER或是GL_FRAGMENT_SHADER。
也有更多其他功能的type,可參照官方文件點我

glShaderSource(shader_id, how_many_string_array, string_array, string_array_length);
再來我們要把剛剛載入的程式碼送進去OpenGL的shader object裡面。
要注意,一份source code一般就放在一個string,string array是說有很多份source code。
第一個是你的shader的handle或是代號,就類似glut裡的window number那樣的東西。
第二個how_many_string_array,不是說有幾行程式碼,像我們一個shader才用一份程式碼就寫1。
第三個string_array結構是const char**,結構是字串陣列,所以只有一個字串的話就要記得加上&。
第四個也是要用string array才會用上的,一般寫NULL就好。

glCompileShader(shader_id);
用跟glShaderSource一樣的ID就可以compile那個shader。

glGetShaderInfoLog
如註解不贅述,可用可不用。


Step3-2


再來要做的是,把vertex shader和fragment shader連結,做成program。
// 用來儲存shader還有program的id
unsigned int vs, fs, program;

void initShader(const char* vname, const char* fname) 
{ 
	std::string source; 

	// 把程式碼讀進source
	loadFile(vname, source); 
	// 編譯shader並且把id傳回vs
	vs = loadShader(source, GL_VERTEX_SHADER);
	source = "";
	loadFile(fname, source);
	fs = loadShader(source, GL_FRAGMENT_SHADER);

	// 創建一個program
	program = glCreateProgram(); 
	// 把vertex shader跟program連結上
	glAttachShader(program, vs);
	// 把fragment shader跟program連結上
	glAttachShader(program, fs);
	// 根據被連結上的shader, link出各種processor
	glLinkProgram(program);
	// 然後使用它
	glUseProgram(program);
}

glCreateProgram();
創建一個program,算是乘載著各種shader的東西。

glAttachShader(program_id, shader_id);
把你compile好的shader連上program,可以連上複數個shader。

glLinkProgram(program_id);
各種shader對應的processor可參照官方文件點我

glUseProgram(program_id);
使用這個program。

那這樣子GLSL裡面shader的基礎再基礎的建置介紹大概就這樣完成了,依照Step1寫的fragment shader,在這個程式裡不管畫什麼都會是紅色的。

之後若想要設定不同點不同顏色,或是貼材質,製作光影等等各種更高階的運用,就要正式介紹shader的程式設計,這在之後幾篇會陸續介紹。

網路上很多範例的shader程式碼,雖然都是外文的,但不算太難,附上一個我覺得很不錯的youtube教程(雖然他唸英文很快),他講得都很詳細而且會順便連數學都一起教你XD



附上本篇程式範例碼(點我下載),沒意外的話會輸出Step1第三張圖的那個程式。

只是要再創建兩個檔案命名為vertex.vs和fragment.frag,內容如Step1,然後放在跟主程式相同目錄就能運行了。
/* * * * * * * * * * * * * * * * * * * 
* 2015/11/12	Author: Director. kk
* glut: ver 3.7.6
* glew: ver 1.13.0
*
*/
#include <string>
#include <iostream>
#include <fstream>

#include <GL\glew.h>	// 依個人路徑include
#include <GL\glut.h>

#pragma comment(lib, "glew32.lib")
#pragma comment(lib, "glut32.lib")

void loadFile(const char* filename, std::string &string) 
{ 
	std::ifstream fp(filename); 
	if(!fp.is_open()){ 
		std::cout << "Open <" << filename << "> error." << std::endl; 
		return; 
	} 

	char temp[300]; 
	while(!fp.eof()){ 
		fp.getline(temp, 300); 
		string += temp; 
		string += '\n'; 
	} 

	fp.close(); 
} 

unsigned int loadShader(std::string &source, GLenum type) 
{ 
	// 告訴OpenGL我們要創的是哪種shader 
	unsigned int ShaderID; 
	ShaderID = glCreateShader(type);
	// 把std::string結構轉換成const char* 
	const char* csource = source.c_str();
	// 把程式碼放進去剛剛創建的shader object中 
	glShaderSource(ShaderID, 1, &csource, NULL);
	// 編譯shader 
	glCompileShader(ShaderID);
	// 這是編譯過程的訊息, 錯誤什麼的把他丟到error裡面 
	char error[1000] = ""; 
	glGetShaderInfoLog(ShaderID, 1000, NULL, error);
	// 然後輸出出來 
	std::cout << "Complie status: \n" << error << std::endl;

	return ShaderID; 
}
// 用來儲存shader還有program的id
unsigned int vs, fs, program;

void initShader(const char* vname, const char* fname) 
{ 
	std::string source; 

	// 把程式碼讀進source
	loadFile(vname, source); 
	// 編譯shader並且把id傳回vs
	vs = loadShader(source, GL_VERTEX_SHADER);
	source = "";
	loadFile(fname, source);
	fs = loadShader(source, GL_FRAGMENT_SHADER);

	// 創建一個program
	program = glCreateProgram(); 
	// 把vertex shader跟program連結上
	glAttachShader(program, vs);
	// 把fragment shader跟program連結上
	glAttachShader(program, fs);
	// 根據被連結上的shader, link出各種processor
	glLinkProgram(program);
	// 然後使用它
	glUseProgram(program);
}

// 回收資源
void clean() 
{ 
	glDetachShader(program, vs); 
	glDetachShader(program, fs); 
	glDeleteShader(vs); 
	glDeleteShader(fs); 
	glDeleteProgram(program); 
} 

void glRenderScene()
{
	glClearColor(0.0, 0.0, 0.0, 1.0); 
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 

	glViewport(0, 0, 400, 400); 

	glMatrixMode(GL_PROJECTION); 
	glLoadIdentity(); 
	gluPerspective(45.0, 400/400.0, 0.1, 1000);

	glMatrixMode(GL_MODELVIEW); 
	glLoadIdentity(); 

	gluLookAt(20, 20, -20, 0, 0, 0, 0, 1, 0); 

	glBegin(GL_QUADS); 
	glVertex3f(10, 0, -10); 
	glVertex3f(-10, 0, -10); 
	glVertex3f(-10, 0, 10); 
	glVertex3f(10, 0, 10); 
	glEnd(); 

	glutSwapBuffers(); 
}

int main(int argc, char **argv)
{
	glutInit(&argc, argv);

	glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGB); 
	glutInitWindowSize(400, 400); 
	glutInitWindowPosition(0, 0); 
	glutCreateWindow("glsl lesson 01"); 

	glewInit();

	glutDisplayFunc(glRenderScene);

	initShader("vertex.vs", "fragment.frag");

	glutMainLoop();

	clean();

	return 0;
}

2 則留言:

  1. 太感謝你了 寫得很棒很詳細

    回覆刪除
  2. 有沒有遇到過這樣的問題~
    已經跟了以上的步驟
    1>------ Build started: Project: lesson1, Configuration: Debug Win32 ------
    1>lesson1.obj : warning LNK4075: ignoring '/EDITANDCONTINUE' due to '/SAFESEH' specification
    1>LINK : fatal error LNK1104: cannot open file 'freeglutd.lib'
    ========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========

    回覆刪除