# 第二课:画第一个三角形
# 第二课: 画第一个三角形
这将又是一篇长教程。
用OpenGL 3实现复杂的东西很方便;为此付出的代价是,画一个简单的三角形变得比较麻烦。
不要忘了,定期复制粘贴,跑一下代码。
> 如果程序启动时崩溃了,很可能是你从错误的目录下运行了它。请仔细地阅读第一课中讲到的如何配置Visual Studio!
## 顶点数组对象(VAO)
你需要创建一个顶点数组对象,并将它设为当前对象(细节暂不深入):
```
<pre class="calibre16">```
GLuint VertexArrayID<span class="token1">;</span>
<span class="token3">glGenVertexArrays</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>VertexArrayID<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindVertexArray</span><span class="token1">(</span>VertexArrayID<span class="token1">)</span><span class="token1">;</span>
```
```
当窗口创建成功后(即OpenGL上下文创建后),马上做这一步工作;必须在任何其他OpenGL调用前完成。
若想进一步了解顶点数组对象(VAO),可以参考其他教程;但这不是很重要。
## 屏幕坐标系
三点定义一个三角形。当我们在三维图形学中谈论“点(point)”时,我们经常说“顶点(Vertex)”。一个顶点有三个坐标:X,Y和Z。你可以用以下方式来想象这三个坐标:
X 在你的右方Y 在你的上方Z 是你背后的方向(是的,背后,而不是你的前方)这里有一个更形象的方法:使用右手定则
X 是你的拇指Y 是你的食指Z 是你的中指。如果你把你的拇指指向右边,食指指向天空,那么中指将指向你的背后。让Z指往这个方向很奇怪,为什么要这样呢?简单的说:因为基于右手定则的坐标系被广泛使用了100多年,它会给你很多有用的数学工具;而唯一的缺点只是Z方向不直观。
`补充:`注意,你可以自由地移动你的手:你的X,Y和Z轴也将跟着移动(详见后文)。
我们需要三个三维点来组成一个三角形;现在开始:
```
<pre class="calibre16">```
<span class="token2">// An array of 3 vectors which represents 3 vertices</span>
static const GLfloat g_vertex_buffer_data<span class="token1">[</span><span class="token1">]</span> <span class="token">=</span> <span class="token1">{</span>
<span class="token">-</span><span class="token6">1.0</span>f<span class="token1">,</span> <span class="token">-</span><span class="token6">1.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token6">1.0</span>f<span class="token1">,</span> <span class="token">-</span><span class="token6">1.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token6">0.0</span>f<span class="token1">,</span><span class="token">?</span> <span class="token6">1.0</span>f<span class="token1">,</span> <span class="token6">0.0</span>f<span class="token1">,</span>
<span class="token1">}</span><span class="token1">;</span>
```
```
第一个顶点是(-1, -1, 0)。
这意味着除非我们以某种方式变换它,否则它将显示在屏幕的(-1, -1)位置。什么意思呢?屏幕的原点在中间,X在右方,Y在上方。屏幕坐标如下图:
![](https://box.kancloud.cn/2015-11-02_5636f301c85be.png)
该机制内置于显卡,无法改变。因此(-1, -1)是屏幕的左下角,(1, -1)是右下角,(0, 1)在中上位置。这个三角形应该占满了大部分屏幕。
## 画我们的三角形
下一步把这个三角形传给OpenGL。我们通过创建一个缓冲区完成:
```
<pre class="calibre16">```
<span class="token2">// This will identify our vertex buffer</span>
GLuint vertexbuffer<span class="token1">;</span>
<span class="token2">// Generate 1 buffer, put the resulting identifier in vertexbuffer</span>
<span class="token3">glGenBuffers</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>vertexbuffer<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// The following commands will talk about our 'vertexbuffer' buffer</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> vertexbuffer<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Give our vertices to OpenGL.</span>
<span class="token3">glBufferData</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> <span class="token3">sizeof</span><span class="token1">(</span>g_vertex_buffer_data<span class="token1">)</span><span class="token1">,</span> g_vertex_buffer_data<span class="token1">,</span> GL_STATIC_DRAW<span class="token1">)</span><span class="token1">;</span>
```
```
这只要做一次。
现在,我们的主循环中,那个之前啥都没有的地方,就能画我们宏伟的三角形了:
```
<pre class="calibre16">```
<span class="token2">// 1rst attribute buffer : vertices</span>
<span class="token3">glEnableVertexAttribArray</span><span class="token1">(</span><span class="token6">0</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindBuffer</span><span class="token1">(</span>GL_ARRAY_BUFFER<span class="token1">,</span> vertexbuffer<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glVertexAttribPointer</span><span class="token1">(</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// attribute 0. No particular reason for 0, but must match the layout in the shader.</span>
<span class="token6">3</span><span class="token1">,</span> <span class="token2">// size</span>
GL_FLOAT<span class="token1">,</span> <span class="token2">// type</span>
GL_FALSE<span class="token1">,</span> <span class="token2">// normalized?</span>
<span class="token6">0</span><span class="token1">,</span> <span class="token2">// stride</span>
<span class="token1">(</span>void<span class="token">*</span><span class="token1">)</span><span class="token6">0</span> <span class="token2">// array buffer offset</span>
<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Draw the triangle !</span>
<span class="token3">glDrawArrays</span><span class="token1">(</span>GL_TRIANGLES<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span> <span class="token6">3</span><span class="token1">)</span><span class="token1">;</span> <span class="token2">// Starting from vertex 0; 3 vertices total -> 1 triangle</span>
<span class="token3">glDisableVertexAttribArray</span><span class="token1">(</span><span class="token6">0</span><span class="token1">)</span><span class="token1">;</span>
```
```
结果如图:
![](https://box.kancloud.cn/2015-11-02_5636f301d42d1.png)
白色略显无聊。让我们来看看怎么把它涂成红色。这就需要用到一个叫『着色器(Shader)』的东西。
## 着色器
### 编译着色器
在最简单的配置下,你将需要两个着色器:一个叫顶点着色器,它将作用于每个顶点上;另一个叫片断(Fragment)着色器,它将作用于每一个采样点。我们使用4倍反走样,因此每像素有四个采样点。
着色器编程使用GLSL(GL Shader Language,GL着色语言),它是OpenGL的一部分。与C或Java不同,GLSL必须在运行时编译,这意味着每次启动程序,所有的着色器将重新编译。
这两个着色器通常放在单独的文件里。本例中,我们有SimpleFragmentShader.fragmentshader和SimpleVertexShader.vertexshader两个着色器。他们的扩展名是无关紧要的,可以是.txt或者.glsl。
以下是代码。完全理解它不是很重要,因为通常一个程序只做一次,看懂注释就够了。所有其他课程代码都用到了这个函数,所以它被放在一个单独的文件中:common/loadShader.cpp。注意,和缓冲区一样,着色器不能直接访问:我们仅仅有一个编号(ID)。真正的实现隐藏在驱动程序中。
```
<pre class="calibre16">```
GLuint <span class="token3">LoadShaders</span><span class="token1">(</span>const char <span class="token">*</span> vertex_file_path<span class="token1">,</span>const char <span class="token">*</span> fragment_file_path<span class="token1">)</span><span class="token1">{</span>
<span class="token2">// Create the shaders</span>
GLuint VertexShaderID <span class="token">=</span> <span class="token3">glCreateShader</span><span class="token1">(</span>GL_VERTEX_SHADER<span class="token1">)</span><span class="token1">;</span>
GLuint FragmentShaderID <span class="token">=</span> <span class="token3">glCreateShader</span><span class="token1">(</span>GL_FRAGMENT_SHADER<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Read the Vertex Shader code from the file</span>
std<span class="token1">:</span><span class="token1">:</span>string VertexShaderCode<span class="token1">;</span>
std<span class="token1">:</span><span class="token1">:</span>ifstream <span class="token3">VertexShaderStream</span><span class="token1">(</span>vertex_file_path<span class="token1">,</span> std<span class="token1">:</span><span class="token1">:</span>ios<span class="token1">:</span><span class="token1">:</span><span class="token4">in</span><span class="token1">)</span><span class="token1">;</span>
<span class="token4">if</span><span class="token1">(</span>VertexShaderStream<span class="token1">.</span><span class="token3">is_open</span><span class="token1">(</span><span class="token1">)</span><span class="token1">)</span>
<span class="token1">{</span>
std<span class="token1">:</span><span class="token1">:</span>string Line <span class="token">=</span> <span class="token5">""</span><span class="token1">;</span>
<span class="token4">while</span><span class="token1">(</span><span class="token3">getline</span><span class="token1">(</span>VertexShaderStream<span class="token1">,</span> Line<span class="token1">)</span><span class="token1">)</span>
VertexShaderCode <span class="token">+</span><span class="token">=</span> <span class="token5">"\n"</span> <span class="token">+</span> Line<span class="token1">;</span>
VertexShaderStream<span class="token1">.</span><span class="token3">close</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
<span class="token1">}</span>
<span class="token2">// Read the Fragment Shader code from the file</span>
std<span class="token1">:</span><span class="token1">:</span>string FragmentShaderCode<span class="token1">;</span>
std<span class="token1">:</span><span class="token1">:</span>ifstream <span class="token3">FragmentShaderStream</span><span class="token1">(</span>fragment_file_path<span class="token1">,</span> std<span class="token1">:</span><span class="token1">:</span>ios<span class="token1">:</span><span class="token1">:</span><span class="token4">in</span><span class="token1">)</span><span class="token1">;</span>
<span class="token4">if</span><span class="token1">(</span>FragmentShaderStream<span class="token1">.</span><span class="token3">is_open</span><span class="token1">(</span><span class="token1">)</span><span class="token1">)</span><span class="token1">{</span>
std<span class="token1">:</span><span class="token1">:</span>string Line <span class="token">=</span> <span class="token5">""</span><span class="token1">;</span>
<span class="token4">while</span><span class="token1">(</span><span class="token3">getline</span><span class="token1">(</span>FragmentShaderStream<span class="token1">,</span> Line<span class="token1">)</span><span class="token1">)</span>
FragmentShaderCode <span class="token">+</span><span class="token">=</span> <span class="token5">"\n"</span> <span class="token">+</span> Line<span class="token1">;</span>
FragmentShaderStream<span class="token1">.</span><span class="token3">close</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
<span class="token1">}</span>
GLint Result <span class="token">=</span> GL_FALSE<span class="token1">;</span>
int InfoLogLength<span class="token1">;</span>
<span class="token2">// Compile Vertex Shader</span>
<span class="token3">printf</span><span class="token1">(</span><span class="token5">"Compiling shader : %s\n"</span><span class="token1">,</span> vertex_file_path<span class="token1">)</span><span class="token1">;</span>
char const <span class="token">*</span> VertexSourcePointer <span class="token">=</span> VertexShaderCode<span class="token1">.</span><span class="token3">c_str</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glShaderSource</span><span class="token1">(</span>VertexShaderID<span class="token1">,</span> <span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>VertexSourcePointer <span class="token1">,</span> NULL<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glCompileShader</span><span class="token1">(</span>VertexShaderID<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Check Vertex Shader</span>
<span class="token3">glGetShaderiv</span><span class="token1">(</span>VertexShaderID<span class="token1">,</span> GL_COMPILE_STATUS<span class="token1">,</span> <span class="token">&</span>Result<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glGetShaderiv</span><span class="token1">(</span>VertexShaderID<span class="token1">,</span> GL_INFO_LOG_LENGTH<span class="token1">,</span> <span class="token">&</span>InfoLogLength<span class="token1">)</span><span class="token1">;</span>
std<span class="token1">:</span><span class="token1">:</span>vector <span class="token3">VertexShaderErrorMessage</span><span class="token1">(</span>InfoLogLength<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glGetShaderInfoLog</span><span class="token1">(</span>VertexShaderID<span class="token1">,</span> InfoLogLength<span class="token1">,</span> NULL<span class="token1">,</span> <span class="token">&</span>VertexShaderErrorMessage<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">fprintf</span><span class="token1">(</span>stdout<span class="token1">,</span> <span class="token5">"%s\n"</span><span class="token1">,</span> <span class="token">&</span>VertexShaderErrorMessage<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Compile Fragment Shader</span>
<span class="token3">printf</span><span class="token1">(</span><span class="token5">"Compiling shader : %s\n"</span><span class="token1">,</span> fragment_file_path<span class="token1">)</span><span class="token1">;</span>
char const <span class="token">*</span> FragmentSourcePointer <span class="token">=</span> FragmentShaderCode<span class="token1">.</span><span class="token3">c_str</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glShaderSource</span><span class="token1">(</span>FragmentShaderID<span class="token1">,</span> <span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>FragmentSourcePointer <span class="token1">,</span> NULL<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glCompileShader</span><span class="token1">(</span>FragmentShaderID<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Check Fragment Shader</span>
<span class="token3">glGetShaderiv</span><span class="token1">(</span>FragmentShaderID<span class="token1">,</span> GL_COMPILE_STATUS<span class="token1">,</span> <span class="token">&</span>Result<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glGetShaderiv</span><span class="token1">(</span>FragmentShaderID<span class="token1">,</span> GL_INFO_LOG_LENGTH<span class="token1">,</span> <span class="token">&</span>InfoLogLength<span class="token1">)</span><span class="token1">;</span>
std<span class="token1">:</span><span class="token1">:</span>vector <span class="token3">FragmentShaderErrorMessage</span><span class="token1">(</span>InfoLogLength<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glGetShaderInfoLog</span><span class="token1">(</span>FragmentShaderID<span class="token1">,</span> InfoLogLength<span class="token1">,</span> NULL<span class="token1">,</span> <span class="token">&</span>FragmentShaderErrorMessage<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">fprintf</span><span class="token1">(</span>stdout<span class="token1">,</span> <span class="token5">"%s\n"</span><span class="token1">,</span> <span class="token">&</span>FragmentShaderErrorMessage<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Link the program</span>
<span class="token3">fprintf</span><span class="token1">(</span>stdout<span class="token1">,</span> <span class="token5">"Linking programn"</span><span class="token1">)</span><span class="token1">;</span>
GLuint ProgramID <span class="token">=</span> <span class="token3">glCreateProgram</span><span class="token1">(</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glAttachShader</span><span class="token1">(</span>ProgramID<span class="token1">,</span> VertexShaderID<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glAttachShader</span><span class="token1">(</span>ProgramID<span class="token1">,</span> FragmentShaderID<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glLinkProgram</span><span class="token1">(</span>ProgramID<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Check the program</span>
<span class="token3">glGetProgramiv</span><span class="token1">(</span>ProgramID<span class="token1">,</span> GL_LINK_STATUS<span class="token1">,</span> <span class="token">&</span>Result<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glGetProgramiv</span><span class="token1">(</span>ProgramID<span class="token1">,</span> GL_INFO_LOG_LENGTH<span class="token1">,</span> <span class="token">&</span>InfoLogLength<span class="token1">)</span><span class="token1">;</span>
std<span class="token1">:</span><span class="token1">:</span>vector <span class="token3">ProgramErrorMessage</span><span class="token1">(</span> <span class="token3">max</span><span class="token1">(</span>InfoLogLength<span class="token1">,</span> <span class="token3">int</span><span class="token1">(</span><span class="token6">1</span><span class="token1">)</span><span class="token1">)</span> <span class="token1">)</span><span class="token1">;</span>
<span class="token3">glGetProgramInfoLog</span><span class="token1">(</span>ProgramID<span class="token1">,</span> InfoLogLength<span class="token1">,</span> NULL<span class="token1">,</span> <span class="token">&</span>ProgramErrorMessage<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">fprintf</span><span class="token1">(</span>stdout<span class="token1">,</span> <span class="token5">"%s\n"</span><span class="token1">,</span> <span class="token">&</span>ProgramErrorMessage<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glDeleteShader</span><span class="token1">(</span>VertexShaderID<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glDeleteShader</span><span class="token1">(</span>FragmentShaderID<span class="token1">)</span><span class="token1">;</span>
<span class="token4">return</span> ProgramID<span class="token1">;</span>
<span class="token1">}</span>
```
```
### 我们的顶点着色器
我们先写顶点着色器。
第一行告诉编译器我们将用OpenGL 3的语法。
```
<pre class="calibre16">```
#version <span class="token6">330</span> core
```
```
第二行声明输入数据:
```
<pre class="calibre16">```
<span class="token3">layout</span><span class="token1">(</span>location <span class="token">=</span> <span class="token6">0</span><span class="token1">)</span> <span class="token4">in</span> vec3 vertexPosition_modelspace<span class="token1">;</span>
```
```
具体解释一下这一行:
“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语言一样):
```
<pre class="calibre16">```
void <span class="token3">main</span><span class="token1">(</span><span class="token1">)</span><span class="token1">{</span>
```
```
我们的main函数只是将顶点的位置设为缓冲区里的值,无论这值是多少。因此如果我们给出位置(1,1),那么三角形将有一个顶点在屏幕的右上角。在下一课中我们将看到,怎样对输入位置做一些更有趣的计算。
```
<pre class="calibre16">```
gl_Position<span class="token1">.</span>xyz <span class="token">=</span> vertexPosition_modelspace<span class="token1">;</span>
gl_Position<span class="token1">.</span>w <span class="token">=</span> <span class="token6">1.0</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
gl\_Position是为数不多的内置变量之一:你必须赋一个值给它。其他操作都是可选的,我们将在第四课中看到“其他操作”指的是什么。
### 我们的片断着色器
作为我们的第一个片断着色器,我们只做一个简单的事:设置每个片断的颜色为红色。(记住,每像素有4个片断,因为我们用的是4倍反走样)
```
<pre class="calibre16">```
out vec3 color<span class="token1">;</span>
void <span class="token3">main</span><span class="token1">(</span><span class="token1">)</span><span class="token1">{</span>
color <span class="token">=</span> <span class="token3">vec3</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span><span class="token6">0</span><span class="token1">,</span><span class="token6">0</span><span class="token1">)</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
vec3(1,0,0)代表红色。因为在计算机屏幕上,颜色由红,绿,蓝这个顺序三元组表示。因此(1,0,0)意思是全红,没有绿色,也没有蓝色。
## 把它们组合起来
在main循环前,调用我们的LoadShaders函数:
```
<pre class="calibre16">```
<span class="token2">// Create and compile our GLSL program from the shaders</span>
GLuint programID <span class="token">=</span> <span class="token3">LoadShaders</span><span class="token1">(</span> <span class="token5">"SimpleVertexShader.vertexshader"</span><span class="token1">,</span> <span class="token5">"SimpleFragmentShader.fragmentshader"</span> <span class="token1">)</span><span class="token1">;</span>
```
```
现在在main循环中,首先清屏:
```
<pre class="calibre16">```
<span class="token3">glClear</span><span class="token1">(</span>GL_COLOR_BUFFER_BIT <span class="token">|</span> GL_DEPTH_BUFFER_BIT<span class="token1">)</span><span class="token1">;</span>
```
```
然后告诉OpenGL你想用你的着色器:
```
<pre class="calibre16">```
<span class="token2">// Use our shader</span>
<span class="token3">glUseProgram</span><span class="token1">(</span>programID<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Draw triangle...</span>
```
```
…接着转眼间,这就是你的红色三角形!
![](https://box.kancloud.cn/2015-11-02_5636f301e500d.png)
下一课中我们将学习变换:如何设置你的相机,移动物体等等。