# 第二課: 畫第一個三角形
這將又是一篇長教程。
用OpenGL 3實現復雜的東西很方便;為此付出的代價是,畫一個簡單的三角形變得比較麻煩。
不要忘了,定期復制粘貼,跑一下代碼。
> 如果程序啟動時崩潰了,很可能是你從錯誤的目錄下運行了它。請仔細地閱讀第一課中講到的如何配置Visual Studio!
## 頂點數組對象(VAO)
你需要創建一個頂點數組對象,并將它設為當前對象(細節暫不深入):
~~~
GLuint VertexArrayID;
glGenVertexArrays(1, &VertexArrayID);
glBindVertexArray(VertexArrayID);
~~~
當窗口創建成功后(即OpenGL上下文創建后),馬上做這一步工作;必須在任何其他OpenGL調用前完成。
若想進一步了解頂點數組對象(VAO),可以參考其他教程;但這不是很重要。
## 屏幕坐標系
三點定義一個三角形。當我們在三維圖形學中談論“點(point)”時,我們經常說“頂點(Vertex)”。一個頂點有三個坐標:X,Y和Z。你可以用以下方式來想象這三個坐標:
X 在你的右方Y 在你的上方Z 是你背后的方向(是的,背后,而不是你的前方)這里有一個更形象的方法:使用右手定則
X 是你的拇指Y 是你的食指Z 是你的中指。如果你把你的拇指指向右邊,食指指向天空,那么中指將指向你的背后。讓Z指往這個方向很奇怪,為什么要這樣呢?簡單的說:因為基于右手定則的坐標系被廣泛使用了100多年,它會給你很多有用的數學工具;而唯一的缺點只是Z方向不直觀。
`補充:`注意,你可以自由地移動你的手:你的X,Y和Z軸也將跟著移動(詳見后文)。
我們需要三個三維點來組成一個三角形;現在開始:
~~~
// An array of 3 vectors which represents 3 vertices
static const GLfloat g_vertex_buffer_data[] = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
0.0f,? 1.0f, 0.0f,
};
~~~
第一個頂點是(-1, -1, 0)。
這意味著除非我們以某種方式變換它,否則它將顯示在屏幕的(-1, -1)位置。什么意思呢?屏幕的原點在中間,X在右方,Y在上方。屏幕坐標如下圖:

該機制內置于顯卡,無法改變。因此(-1, -1)是屏幕的左下角,(1, -1)是右下角,(0, 1)在中上位置。這個三角形應該占滿了大部分屏幕。
## 畫我們的三角形
下一步把這個三角形傳給OpenGL。我們通過創建一個緩沖區完成:
~~~
// This will identify our vertex buffer
GLuint vertexbuffer;
// Generate 1 buffer, put the resulting identifier in vertexbuffer
glGenBuffers(1, &vertexbuffer);
// The following commands will talk about our 'vertexbuffer' buffer
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
// Give our vertices to OpenGL.
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);
~~~
這只要做一次。
現在,我們的主循環中,那個之前啥都沒有的地方,就能畫我們宏偉的三角形了:
~~~
// 1rst attribute buffer : vertices
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexbuffer);
glVertexAttribPointer(
0, // attribute 0. No particular reason for 0, but must match the layout in the shader.
3, // size
GL_FLOAT, // type
GL_FALSE, // normalized?
0, // stride
(void*)0 // array buffer offset
);
// Draw the triangle !
glDrawArrays(GL_TRIANGLES, 0, 3); // Starting from vertex 0; 3 vertices total -> 1 triangle
glDisableVertexAttribArray(0);
~~~
結果如圖:

白色略顯無聊。讓我們來看看怎么把它涂成紅色。這就需要用到一個叫『著色器(Shader)』的東西。
## 著色器
### 編譯著色器
在最簡單的配置下,你將需要兩個著色器:一個叫頂點著色器,它將作用于每個頂點上;另一個叫片斷(Fragment)著色器,它將作用于每一個采樣點。我們使用4倍反走樣,因此每像素有四個采樣點。
著色器編程使用GLSL(GL Shader Language,GL著色語言),它是OpenGL的一部分。與C或Java不同,GLSL必須在運行時編譯,這意味著每次啟動程序,所有的著色器將重新編譯。
這兩個著色器通常放在單獨的文件里。本例中,我們有SimpleFragmentShader.fragmentshader和SimpleVertexShader.vertexshader兩個著色器。他們的擴展名是無關緊要的,可以是.txt或者.glsl。
以下是代碼。完全理解它不是很重要,因為通常一個程序只做一次,看懂注釋就夠了。所有其他課程代碼都用到了這個函數,所以它被放在一個單獨的文件中:common/loadShader.cpp。注意,和緩沖區一樣,著色器不能直接訪問:我們僅僅有一個編號(ID)。真正的實現隱藏在驅動程序中。
~~~
GLuint LoadShaders(const char * vertex_file_path,const char * fragment_file_path){
// Create the shaders
GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);
GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);
// Read the Vertex Shader code from the file
std::string VertexShaderCode;
std::ifstream VertexShaderStream(vertex_file_path, std::ios::in);
if(VertexShaderStream.is_open())
{
std::string Line = "";
while(getline(VertexShaderStream, Line))
VertexShaderCode += "\n" + Line;
VertexShaderStream.close();
}
// Read the Fragment Shader code from the file
std::string FragmentShaderCode;
std::ifstream FragmentShaderStream(fragment_file_path, std::ios::in);
if(FragmentShaderStream.is_open()){
std::string Line = "";
while(getline(FragmentShaderStream, Line))
FragmentShaderCode += "\n" + Line;
FragmentShaderStream.close();
}
GLint Result = GL_FALSE;
int InfoLogLength;
// Compile Vertex Shader
printf("Compiling shader : %s\n", vertex_file_path);
char const * VertexSourcePointer = VertexShaderCode.c_str();
glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
glCompileShader(VertexShaderID);
// Check Vertex Shader
glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
std::vector VertexShaderErrorMessage(InfoLogLength);
glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
fprintf(stdout, "%s\n", &VertexShaderErrorMessage[0]);
// Compile Fragment Shader
printf("Compiling shader : %s\n", fragment_file_path);
char const * FragmentSourcePointer = FragmentShaderCode.c_str();
glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
glCompileShader(FragmentShaderID);
// Check Fragment Shader
glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
std::vector FragmentShaderErrorMessage(InfoLogLength);
glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
fprintf(stdout, "%s\n", &FragmentShaderErrorMessage[0]);
// Link the program
fprintf(stdout, "Linking programn");
GLuint ProgramID = glCreateProgram();
glAttachShader(ProgramID, VertexShaderID);
glAttachShader(ProgramID, FragmentShaderID);
glLinkProgram(ProgramID);
// Check the program
glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
std::vector ProgramErrorMessage( max(InfoLogLength, int(1)) );
glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
fprintf(stdout, "%s\n", &ProgramErrorMessage[0]);
glDeleteShader(VertexShaderID);
glDeleteShader(FragmentShaderID);
return ProgramID;
}
~~~
### 我們的頂點著色器
我們先寫頂點著色器。
第一行告訴編譯器我們將用OpenGL 3的語法。
~~~
#version 330 core
~~~
第二行聲明輸入數據:
~~~
layout(location = 0) in vec3 vertexPosition_modelspace;
~~~
具體解釋一下這一行:
“vec3”在GLSL中是一個三維向量。類似于(但不相同)以前我們用來聲明三角形的glm::vec3。最重要的是,如果我們在C++中使用三維向量,那么在GLSL中也使用三維向量。
“layout(location = 0)”指我們用來賦給vertexPosition_modelspace這個屬性的緩沖區。每個頂點能有多種屬性:位置,一種或多種顏色,一個或多個紋理坐標,等等。OpenGL不知道什么是顏色:它只是看到一個vec3。因此我們必須告訴它,哪個緩沖對應哪個輸入。通過將glvertexAttribPointer函數的第一個參數值賦給layout,我們就完成了這一點。參數值“0”并不重要,它可以是12(但是不大于glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &v));重要的是兩邊參數值保持一致。
“vertexPosition_modelspace”這個變量名你可以任取,它將包含每個頂點著色器運行所需的頂點位置值。
“in”的意思是這是一些輸入數據。不久我們將會看到“out”關鍵詞。
每個頂點都會調用main函數(和C語言一樣):
~~~
void main(){
~~~
我們的main函數只是將頂點的位置設為緩沖區里的值,無論這值是多少。因此如果我們給出位置(1,1),那么三角形將有一個頂點在屏幕的右上角。在下一課中我們將看到,怎樣對輸入位置做一些更有趣的計算。
~~~
gl_Position.xyz = vertexPosition_modelspace;
gl_Position.w = 1.0;
}
~~~
gl_Position是為數不多的內置變量之一:你必須賦一個值給它。其他操作都是可選的,我們將在第四課中看到“其他操作”指的是什么。
### 我們的片斷著色器
作為我們的第一個片斷著色器,我們只做一個簡單的事:設置每個片斷的顏色為紅色。(記住,每像素有4個片斷,因為我們用的是4倍反走樣)
~~~
out vec3 color;
void main(){
color = vec3(1,0,0);
}
~~~
vec3(1,0,0)代表紅色。因為在計算機屏幕上,顏色由紅,綠,藍這個順序三元組表示。因此(1,0,0)意思是全紅,沒有綠色,也沒有藍色。
## 把它們組合起來
在main循環前,調用我們的LoadShaders函數:
~~~
// Create and compile our GLSL program from the shaders
GLuint programID = LoadShaders( "SimpleVertexShader.vertexshader", "SimpleFragmentShader.fragmentshader" );
~~~
現在在main循環中,首先清屏:
~~~
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
~~~
然后告訴OpenGL你想用你的著色器:
~~~
// Use our shader
glUseProgram(programID);
// Draw triangle...
~~~
…接著轉眼間,這就是你的紅色三角形!

下一課中我們將學習變換:如何設置你的相機,移動物體等等。