1
0
mirror of https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git synced 2025-08-23 04:35:28 +08:00

校对 01/01 到 01/04

This commit is contained in:
Geequlim
2015-07-26 21:31:06 +08:00
parent 71c6e392c4
commit ed925f7f7f
5 changed files with 179 additions and 155 deletions

View File

@@ -1,42 +1,47 @@
本文作者JoeyDeVries由[Django](http://bullteacher.com/5-hello-triangle.html)翻译自[http://learnopengl.com](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
## 你好三角形
# Hello Triangle
原文 | [Creating a window](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
---|---
作者 | JoeyDeVries
翻译 | [Django](http://bullteacher.com/)
校对 | Geequlim
在OpenGL中任何事物都在3D空间中但是屏幕和窗口是一个2D像素阵列所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你的屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形输送管道**(pipeline大多译为管线实际上指的是一堆原始图形数据途经一个输送管道期间经过各种变化处理最终出现在屏幕的过程)管理的。图形输送管道可以被划分为两个主要部分第一个部分把你的3D坐标转换为2D坐标第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里我们会简单地讨论一下图形输送管道以及如何使用它创建一些像素这对我们来说是有好处的。
### 图形渲染管线
在OpenGL中任何事物都在3D空间中但是屏幕和窗口是一个2D像素阵列所以OpenGL的大部分工作都是关于如何把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的**图形渲染管线**(pipeline大多译为管线实际上指的是一堆原始图形数据途经一个输送管道期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分第一个部分把你的3D坐标转换为2D坐标第二部分是把2D坐标转变为实际的有颜色的像素。这个教程里我们会简单地讨论一下图形渲染管线以及如何使用它创建一些像素这对我们来说是有好处的。
!!! Important
2D坐标和像素也是不同的2D坐标是在2D空间中的一个点的非常精确的表达2D像素是这个点的近似它受到你的屏幕/窗口解析度的限制。
2D坐标和像素也是不同的2D坐标是在2D空间中的一个点的非常精确的表达2D像素是这个点的近似,它受到你的屏幕/窗口解析度的限制。
图形输送管道接收一组3D坐标然后把它们转变为你屏幕上的有色2D像素。图形输送管道可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行的特征当今大多数显卡都有成千上万的小处理核心在GPU上为每一个输送管道的)阶段运行各自的小程序,从而在图形输送管道中快速处理你的数据。这些小程序叫做**着色器**(Shader)。
图形渲染管线接收一组3D坐标然后把它们转变为你屏幕上的有色2D像素。图形渲染管线可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行特性当今大多数显卡都有成千上万的小处理核心在GPU上为每一个渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做 **着色器**(Shader)。
有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制输送管道的特定部分了因为它们运行在GPU上它们会节约宝贵的CPU时间。着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下节会花更多时间研究它。
有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了因为它们运行在GPU上所以它们会节约宝贵的CPU时间。OpenGL着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下节会花更多时间研究它。
在下面,你会看到一个图形输送管道的每个阶段的抽象表达。要注意蓝色部分代表的是我们可以自定义的着色器。
在下面,你会看到一个图形渲染管线的每个阶段的抽象表达。要注意蓝色部分代表的是我们可以自定义的着色器。
![](http://bullteacher.com/wp-content/uploads/2015/05/pipeline.gif)
![](http://geequlim.com/assets/img/blog/LearnOpenGL/01 Getting started/OpenGL_pipline_cn.png)
如你所见,图形输入管道包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释输送管道的每个部分,从而使你对输送管道的工作方式有个大概了解。
如你所见,图形渲染管线包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释渲染管线的每个部分,从而使你对图形渲染管线的工作方式有个大概了解。
我们以数组的形式传递3个3D坐标作为图形输送管道的输入它用来表示一个三角形这个数组叫做顶点数据Vertex Data这里顶点数据是一些顶点的集合。一个**顶点**是一个3D坐标的集合也就是x、y、z数据。而顶点数据是用**顶点属性**vertex attributes表示的它可以包含任何我们希望用的数据但是简单起见我们还是假定每个顶点只由一个3D位置①和几个颜色值组成的吧。
我们以数组的形式传递3个3D坐标作为图形渲染管线的输入它用来表示一个三角形这个数组叫做顶点数据Vertex Data这里顶点数据是一些顶点的集合。一个**顶点**是一个3D坐标的集合也就是x、y、z数据。而顶点数据是用**顶点属性**vertex attributes表示的它可以包含任何我们希望用的数据但是简单起见我们还是假定每个顶点只由一个3D位置①和几个颜色值组成的吧。
①译注:当我们谈论一个“位置”的时候它代表在一个“空间”中所处地点的这个特殊属性同时“空间”代表着任何一种坐标系比如x、y、z三维坐标系x、y二维坐标系或者一条直线上的x和y的线性关系只不过二维坐标系是一个扁扁的平面空间而一条直线是一个很瘦的长长的空间。
!!! Important
为了让OpenGL知道我们的坐标和颜色值构成的到底是什么OpenGL需要你去提示你希望这些数据所表示的是什么类型。我们是希望把这些数据渲染成一系列的点一系列的三角形还是仅仅是一个长长的线做出的这些提示叫做**基本图形**(Primitives)任何一个绘制命令的调用都必须把基本图形类型传递给OpenGL。这是其中的几个`GL_POINTS``GL_TRIANGLES``GL_LINE_STRIP`
为了让OpenGL知道我们的坐标和颜色值构成的到底是什么OpenGL需要你去提示你希望这些数据所表示的是什么类型。我们是希望把这些数据渲染成一系列的点一系列的三角形还是仅仅是一个长长的线做出的这些提示叫做**基本图形**(Primitives)任何一个绘制命令的调用都必须把基本图形类型传递给OpenGL。这是其中的几个**GL_POINTS**、**GL_TRIANGLES**、**GL_LINE_STRIP**
输送管道的第一个部分是**顶点着色器**(Vertex Shader)它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标后面会解释同时顶点着色器允许我们对顶点属性进行一些基本处理。
图形渲染管线的第一个部分是**顶点着色器**(Vertex Shader)它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标后面会解释同时顶点着色器允许我们对顶点属性进行一些基本处理。
**基本图形装**Primitive Assembly阶段把顶点着色器的表示为基本图形的所有顶点作为输入如果选择的是`GL_POINTS`,那么就是一个单独顶点),把所有点组装为特定的基本图形的形状;本节例子是一个三角形。
**基本图形装**Primitive Assembly阶段把顶点着色器的表示为基本图形的所有顶点作为输入如果选择的是`GL_POINTS`,那么就是一个单独顶点),把所有点组装为特定的基本图形的形状;本节例子是一个三角形。
基本图形装阶段的输出会传递给**几何着色器**Geometry Shader。几何着色器把基本图形形式的一系列顶点的集合作为输入它可以通过产生新顶点构造出新的或是其他的基本图形来生成其他形状。例子中它生成了另一个三角形。
基本图形装阶段的输出会传递给**几何着色器**Geometry Shader。几何着色器把基本图形形式的一系列顶点的集合作为输入它可以通过产生新顶点构造出新的或是其他的基本图形来生成其他形状。例子中它生成了另一个三角形。
**细分着色器**Tessellation Shaders拥有把给定基本图形**细分**为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
细分着色器的输出会进入**像素化**Rasterization也译为光栅阶段这里它会把基本图形映射为屏幕上相应的像素生成供片段着色器Fragment Shader使用的片段(Fragment)。在片段着色器运行之前,会执行**裁切**clipping。裁切会丢弃超出你的视图以外的那些像素来提升执行效率。
细分着色器的输出会进入**光栅化**Rasterization也译为像素阶段这里它会把基本图形映射为屏幕上相应的像素生成供片段着色器Fragment Shader使用的片段(Fragment)。在片段着色器运行之前,会执行**裁切**clipping。裁切会丢弃超出你的视图以外的那些像素来提升执行效率。
②译注Fragment通常译为片段但从根本上来说它就是带有一些额外信息的像素由于带有额外信息OpenGL就没有给它取名叫“像素”所以“片段”的中文译法比较不准确听起来就像一个fragment可能包含多个像素一样实际上一个fragment只包含一个像素现在你只要简单记住它是像素就行了。后面你会知道除了像素信息以外它还包含了什么。
!!! Important
@@ -44,15 +49,15 @@
**片段着色器**的主要目的是计算一个像素的最终颜色这也是OpenGL高级效果产生的地方。通常片段着色器包含用来计算像素最终颜色的3D场景的一些数据比如光照、阴影、光的颜色等等
在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**Blending阶段。这个阶段检测像素的相应的深度和Stencil后面会讲使用这些来检查这个像素是否在另一个物体的前面或后面如此做到相应取舍。这个阶段也会查**alpha值**alpha值是一个物体的透明度值和物体之间的**混合**Blend。所以即使在片段着色器中计算出来了一个像素所输出的颜色最后的像素颜色在渲染多个三角形的时候也可能完全不同。
在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**Blending阶段。这个阶段检测像素的相应的深度和Stencil后面会讲使用这些来检查这个像素是否在另一个物体的前面或后面如此做到相应取舍。这个阶段也会查**alpha值**alpha值是一个物体的透明度值和物体之间的**混合**Blend。所以即使在片段着色器中计算出来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能完全不同。
正如你所见的那样,图形输送管道非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。
正如你所见的那样,图形渲染管线非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。
在现代OpenGL中我们**必须**定义至少一个顶点着色器和一个片段着色器因为GPU中没有默认的顶点/片段着色器。出于这个原因开始学习现代OpenGL的时候非常困难因为在你能够渲染自己的第一个三角形之前需要一大堆知识。这章结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
在现代OpenGL中我们**必须**定义至少一个顶点着色器和一个片段着色器因为GPU中没有默认的顶点/片段着色器。出于这个原因开始学习现代OpenGL的时候非常困难因为在你能够渲染自己的第一个三角形之前需要一大堆知识。本节结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
## 顶点输入
### 顶点输入
开始绘制一些东西之前我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库所以我们在OpenGL中指定的所有坐标都是在3D坐标里x、y和z。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素OpenGL只是在当它们的3个轴x、y和z在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**Normalized Device Coordinates会最终显示在你的屏幕上所有出了这个范围的都不会显示
开始绘制一些东西之前我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库所以我们在OpenGL中指定的所有坐标都是在3D坐标里x、y和z。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素OpenGL只是在当它们的3个轴x、y和z在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**Normalized Device CoordinatesNDC)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
由于我们希望渲染一个三角形我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标也就是在OpenGL的可见区域中。
@@ -64,9 +69,11 @@ GLfloat vertices[] = {
};
```
由于OpenGL是在3D空间中工作的我们渲染一个2D三角形它的每个顶点都要有一个z坐标0.0。在这样的方式中三角形的每一处的深度depth都一样从而使它看上去就像2D的
由于OpenGL是在3D空间中工作的我们渲染一个2D三角形它的每个顶点都要有一个z坐标0.0。在这样的方式中三角形的每一处的深度depth都一样从而使它看上去就像2D的。
③译注通常深度可以理解为z坐标它代表一个像素在空间中和你的距离如果离你远就可能被别的像素遮挡你就看不到它了它会被丢弃以节省资源。
!!! Important
译注通常深度可以理解为z坐标它代表一个像素在空间中和你的距离如果离你远就可能被别的像素遮挡你就看不到它了它会被丢弃以节省资源。
!!! Important
@@ -74,17 +81,17 @@ GLfloat vertices[] = {
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是**标准化设备坐标**了标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形忽略z轴
![](http://www.learnopengl.com/img/getting-started/ndc.png)
![](http://www.learnopengl.com/img/getting-started/ndc.png)
与通常的屏幕坐标不同y轴正方向上的点和0, 0坐标是这个图像的中心而不是左上角。最后你希望所有变换过的坐标都在这个坐标空间中否则它们就不可见了。
与通常的屏幕坐标不同y轴正方向上的点和0,0坐标是这个图像的中心而不是左上角。最后你希望所有变换过的坐标都在这个坐标空间中否则它们就不可见了。
你的标准化设备坐标接着会变换为**屏幕空间坐标**Screen-space Coordinates这是使用你通过`glViewport`函数提供的数据,进行**视口变换**Viewport Transform完成的。最后的屏幕空间坐标被变换为像素输入到片段着色器。
有了这样的顶点数据,我们会把它作为输入发送给图形输送管道的第一个处理阶段顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据还要配置OpenGL如何解释这些内存并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。
有了这样的顶点数据,我们会把它作为输入数据发送给图形渲染管线的第一个处理阶段顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据还要配置OpenGL如何解释这些内存并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。
我们通过**顶点缓冲对象**Vertex Buffer Objects, VBO管理这个内存它会在GPU内存储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时顶点着色器几乎立即就能获得顶点这非常快。
我们通过**顶点缓冲对象**Vertex Buffer Objects, VBO管理这个内存它会在GPU内存(通常被称为显存)中储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时顶点着色器几乎立即就能获得顶点这非常快。
顶点缓冲对象VBO是我们在[OpenGL教程](http://www.learnopengl.com/#!Getting-Started/OpenGL)中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样这个缓冲有一个独一无二的ID所以我们可以使用`glGenBuffers`函数生成一个缓冲ID
顶点缓冲对象VBO是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样这个缓冲有一个独一无二的ID所以我们可以使用`glGenBuffers`函数生成一个缓冲ID
```c++
GLuint VBO;
@@ -103,19 +110,19 @@ glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
```
`glBufferData`是一个用来把用户定义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(字节);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。
`glBufferData`是一个用来把用户定义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(字节为单位);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。
第四个参数指定了我们希望显卡如何管理给定的数据。有三种形式:
- `GL_STATIC_DRAW`:数据不会或几乎不会改变。
- `GL_STATIC_DRAW` :数据不会或几乎不会改变。
- `GL_DYNAMIC_DRAW`:数据会被改变很多。
- `GL_STREAM_DRAW`:数据每次绘制时都会改变。
- `GL_STREAM_DRAW` :数据每次绘制时都会改变。
三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它使用的类型最好是`GL_STATIC_DRAW`。如果,比如,一个缓冲中的数据将频繁被改变,那么使用的类型就是`GL_DYNAMIC_DRAW`或`GL_STREAM_DRAW`。这样就能确保图形卡把数据放在高速写入的内存部分。
现在我们把顶点数据储存在显卡的内存中用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器来处理这些数据,所以我们开始创建它们吧。
现在我们把顶点数据储存在显卡的内存中用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器来处理这些数据。现在我们开始着手创建它们吧。
## 顶点着色器
### 顶点着色器
顶点着色器是几个着色器中的一个它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。
@@ -132,7 +139,7 @@ void main()
}
```
就像你所看到的那样GLSL看起来很像C。每个着色器都起始于一个版本声明。这是因为OpenGL3.3和更高的GLSL版本号要去匹配OpenGL的版本GLSL420版本对应于OpenGL4.2)。我们同样显式地表示我们会用核心模式(Core Profile)功能
就像你所看到的那样GLSL看起来很像C。每个着色器都起始于一个版本声明。这是因为OpenGL3.3和更高的GLSL版本号要去匹配OpenGL的版本GLSL420版本对应于OpenGL4.2)。我们同样显式地表示我们会用核心模式(Core Profile)。
下一步我们在顶点着色器中声明所有的输入顶点属性使用in关键字。现在我们只关心位置Position数据所以我们只需要一个顶点属性Attribute。GLSL有一个向量数据类型它包含1到4个`float`元素包含的数量可以从它的后缀看出来。由于每个顶点都有一个3D坐标我们就创建一个`vec3`输入变量来表示位置Position。我们同样也指定输入变量的位置值(Location),这是用`layout (location = 0)`来完成的,你后面会看到为什么我们会需要这个位置值。
@@ -146,7 +153,7 @@ void main()
这个顶点着色器可能是能想到的最简单的了因为我们什么都没有处理就把输入数据输出了。在真实的应用里输入数据通常都没有在标准化设备坐标中所以我们首先就必须把它们放进OpenGL的可视区域内。
## 编译一个着色器
### 编译一个着色器
我们已经写了一个顶点着色器源码但是为了OpenGL能够使用它我们必须在运行时动态编译它的源码。
@@ -166,7 +173,7 @@ glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
```
`lShaderSource`函数把着色器对象作为第一个参数来编译它。第二参数指定了源码中有多少个字符串,这里只有一个。第三个参数是顶点着色器真正的源码,我们可以把第四个参数设置为`NULL`。
`glShaderSource`函数把着色器对象作为第一个参数来编译它。第二参数指定了源码中有多少个**字符串**,这里只有一个。第三个参数是顶点着色器真正的源码,我们可以把第四个参数设置为`NULL`。
!!! Important
@@ -184,9 +191,9 @@ glCompileShader(vertexShader);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
如果编译的时候没有任何错误,顶点着色器就被编译了。
如果编译的时候没有任何错误,顶点着色器就被编译成功了。
## 片段着色器(Fragment Shader)
### 片段着色器(Fragment Shader)
片段着色器是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。
@@ -207,7 +214,7 @@ void main()
片段着色器只需要一个输出变量这个变量是一个4元素表示的最终输出颜色的向量我们可以自己计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为`color`。下面我们简单的把一个带有alpha值为1.01.0代表完全不透明)的橘黄的`vec4`赋值给`color`作为输出。
编译片段着色器的过程与顶点着色器相似尽管这次我们使用GL_FRAGMENT_SHADER作为着色器类型
编译片段着色器的过程与顶点着色器相似,尽管这次我们使用`GL_FRAGMENT_SHADER`作为着色器类型:
```c++
GLuint fragmentShader;
@@ -218,7 +225,7 @@ glCompileShader(fragmentShader);
每个着色器现在都编译了剩下的事情是把两个着色器对象链接到一个着色器程序中Shader Program它是用来渲染的。
## 着色器程序
### 着色器程序
着色器程序对象Shader Program Object是多个着色器最后链接的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象然后当渲染物体的时候激活这个着色器程序。激活了的着色器程序的着色器在调用渲染函数时才可用。
@@ -266,9 +273,9 @@ glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
```
现在我们把输入顶点数据发送给GPU指示GPU如何在顶点和片段着色器中处理它。还没结束OpenGL还不知道如何解释内存中的顶点数据以及怎样把顶点数据链接到顶点着色器的属性上。我们告诉OpenGL怎么做。
现在我们把输入顶点数据发送给GPU指示GPU如何在顶点和片段着色器中处理它。还没结束OpenGL还不知道如何解释内存中的顶点数据以及怎样把顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。
## 链接顶点属性
### 链接顶点属性
顶点着色器允许我们以任何我们想要的形式作为顶点属性的输入同样它也具有很强的灵活性这意味着我们必须手动指定我们的输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。这意味着我们必须在渲染前指定OpenGL如何解释顶点数据。
@@ -281,7 +288,7 @@ glDeleteShader(fragmentShader);
- 在这3个值之间没有空隙或其他值。这几个值紧密排列为一个数组。
- 数据中第一个值是缓冲的开始位置。
有了这些知识我们就可以告诉OpenGL如何解释顶点数据了每一个顶点属性我们使用`glVertexAttribPointer`这个函数:
有了这些信息我们就可以告诉OpenGL如何解释顶点数据了每一个顶点属性我们使用`glVertexAttribPointer`这个函数:
```c++
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
@@ -291,11 +298,11 @@ glEnableVertexAttribArray(0);
`glVertexAttribPointer`函数有很多参数,所以我们仔细来了解它们:
- 第一个参数指定我们要配置哪一个顶点属性。记住,我们在顶点着色器中使用`layout(location = 0)`定义了顶点属性——位置Position的位置值(Location)。这样要把顶点属性的位置值(Location)设置为0因为我们希望把数据传递到这个顶点属性中所以我们在这里填0。
- 下一个参数指定顶点属性的大小。顶点属性是`vec3`类型它由3个数值组成。
- 第二个参数指定顶点属性的大小。顶点属性是`vec3`类型它由3个数值组成。
- 第三个参数指定数据的类型,这里是`GL_FLOAT`GLSL中`vec*`是由浮点数组成的)。
- 下个参数定义我们是否希望数据被标准化。如果我们设置为`GL_TRUE`所有数据都会被映射到0对于有符号型signed数据是-1到1之间。我们把它设置为`GL_FALSE`。
- 第五个参数叫做步长Stride它告诉我们在连续的顶点属性之间间隔有多少。由于下个位置数据在3个`GLfloat`后面的位置,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的在两个顶点属性之间没有空隙我们也可以设置为0来让OpenGL决定具体步长是多少只有当数值是紧密排列时才可用。每当我们有更多的顶点属性我们就必须小心地定义每个顶点属性之间的空间我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
- 最后一个参数有古怪的`GLvoid*`的强制类型转换。它我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始所以这里是0。我们会在后面详细解释这个参数。
- 最后一个参数有古怪的`GLvoid*`的强制类型转换。它表示我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始所以这里是0。我们会在后面详细解释这个参数。
!!! Important
@@ -318,9 +325,9 @@ someOpenGLFunctionThatDrawsOurTriangle();
我们绘制一个物体的时候必须重复这件事。这看起来也不多但是如果有超过5个顶点属性100多个不同物体这并不罕见呢。绑定合适的缓冲对象为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有的配置储存在一个对象中并且可以通过绑定这个对象来恢复状态
### 顶点数组对象(Vertex Array Object)
### 顶点数组对象(Vertex Array Object,VAO)
顶点数组对象Vertex Array Object, VAO可以像顶点缓冲对象一样绑定任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处当配置顶点属性指针时你只用做一次每次绘制一个物体的时候我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。
**顶点数组对象Vertex Array Object, VAO**可以像顶点缓冲对象一样绑定任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处当配置顶点属性指针时你只用做一次每次绘制一个物体的时候我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。
!!! Attention
@@ -328,13 +335,13 @@ someOpenGLFunctionThatDrawsOurTriangle();
一个顶点数组对象储存下面的内容:
- 调用`glEnableVertexAttribArray`和`glDisableVertexAttribArray`
- 调用`glEnableVertexAttribArray`和`glDisableVertexAttribArray`。
- 使用`glVertexAttribPointer`的顶点属性配置。
- 使用`glVertexAttribPointer`进行的顶点缓冲对象与顶点属性链接。
![](http://learnopengl.com/img/getting-started/vertex_array_objects.png)
生成一个VAO和生成VBO很像
生成一个VAO和生成VBO类似
```c++
GLuint VAO;
@@ -375,7 +382,7 @@ glBindVertexArray(0);
通常情况下当我们配置好它们以后要解绑OpenGL对象这样我们才不会在某处错误地配置它们。
就是这样前面做的一切都是等待这一刻我们已经把我们的顶点属性配置和打算使用的VBO储存到一个VAO中。一般当你有多个物体打算绘制时你首先要生成/配置所有的VAO它需要VBO和属性指针然后储存它们准备后面使用。当我们打算绘制物体的时候吗我们就拿出相应的VAO绑定它绘制完物体后再解绑VAO。
就是现在前面做的一切都是等待这一刻我们已经把我们的顶点属性配置和打算使用的VBO储存到一个VAO中。一般当你有多个物体打算绘制时你首先要生成/配置所有的VAO它需要VBO和属性指针然后储存它们准备后面使用。当我们打算绘制物体的时候就拿出相应的VAO绑定它绘制完物体后再解绑VAO。
### 我们一直期待的三角形
@@ -398,7 +405,7 @@ glBindVertexArray(0);
如果你的输出和这个不一样,你可能做错了什么,去看源码,看看是否遗漏了什么东西或者在评论部分提问。
## 索引缓冲对象(Element Buffer Objects)
### 索引缓冲对象(Element Buffer ObjectsEBO)
这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象简称EBO或IBO。解释索引缓冲对象的工作方式最好是举例子假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形OpenGL主要就是绘制三角形。这会生成下面的顶点的集合
@@ -419,7 +426,7 @@ GLfloat vertices[] = {
就像你所看到的那样有几个顶点叠加了。我们指定右下角和左上角两次一个矩形只有4个而不是6个顶点这样就产生50%的额外开销。当我们有超过1000个三角的模型这个问题会更糟糕这会产生一大堆浪费。最好的解决方案就是每个顶点只储存一次当我们打算绘制这些顶点的时候只调用顶点的索引。这种情况我们只要储存4个顶点就能绘制矩形了我们只要指定我们打算绘制的那个顶点的索引就行了。如果OpenGL提供这个功能就好了对吧
很幸运索引缓冲的工作方式正是这样的。一个EBO是一个顶点缓冲对象一样的缓冲它专门储存索引OpenGL调用这些顶点的索引来绘制。索引绘制正是这个问题的解决方案。我们先要定义独一无二的顶点和绘制出矩形的索引
很幸运索引缓冲的工作方式正是这样的。一个EBO是一个顶点缓冲对象VBO一样的缓冲它专门储存索引OpenGL调用这些顶点的索引来绘制。索引绘制正是这个问题的解决方案。我们先要定义独一无二的顶点和绘制出矩形的索引
```c++
GLfloat vertices[] = {
@@ -510,15 +517,15 @@ glBindVertexArray(0);
如果用线框模式绘制你的三角你可以配置OpenGL绘制用的基本图形调用`glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)`。第一个参数说:我们打算应用到所有的三角形的前面和背面,第二个参数告诉我们用线来绘制。在随后的绘制函数调用后会一直以线框模式绘制三角形,直到我们用`glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)`设置回了默认模式。
如果你遇到任何错误,回头检查代码,看看是否遗漏了什么。同时,你可以在这里获得全部源码可以在评论区自由提问。
如果你遇到任何错误,回头检查代码,看看是否遗漏了什么。同时,你可以[在这里获得全部源码](http://learnopengl.com/code_viewer.php?code=getting-started/hellotriangle2),也可以在评论区自由提问。
如果你绘制出了这个三角形或矩形那么恭喜你你成功地通过了现代OpenGL最难部分之一绘制你自己的第一个三角形。这部分很难因为在可以绘制第一个三角形之前需要很多知识。幸运的是我们现在已经越过了这个障碍接下来的教程会比较容易理解一些。
## 附加资源
[antongerdelan.net/hellotriangle](http://antongerdelan.net/opengl/hellotriangle.html): 一个渲染第一个三角形的教程。
[open.gl/drawing](https://open.gl/drawing): Alexander Overvoorde的关于渲染第一个三角形的教程。
[antongerdelan.net/vertexbuffers](http://antongerdelan.net/opengl/vertexbuffers.html): 顶点缓冲对象的一些深入探讨。
- [antongerdelan.net/hellotriangle](http://antongerdelan.net/opengl/hellotriangle.html): 一个渲染第一个三角形的教程。
- [open.gl/drawing](https://open.gl/drawing): Alexander Overvoorde的关于渲染第一个三角形的教程。
- [antongerdelan.net/vertexbuffers](http://antongerdelan.net/opengl/vertexbuffers.html): 顶点缓冲对象的一些深入探讨。
# 练习