企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 第二课:画第一个三角形 # 第二课: 画第一个三角形 这将又是一篇长教程。 用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) 下一课中我们将学习变换:如何设置你的相机,移动物体等等。