mirror of
https://github.com/LearnOpenGL-CN/LearnOpenGL-CN.git
synced 2025-08-23 12:45:29 +08:00
01-04,05 Test comments
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
## 你好三角形
|
# 你好三角形
|
||||||
|
|
||||||
原文 | [Creating a window](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
|
原文 | [Creating a window](http://www.learnopengl.com/#!Getting-started/Hello-Triangle)
|
||||||
---|---
|
---|---
|
||||||
@@ -6,15 +6,15 @@
|
|||||||
翻译 | [Django](http://bullteacher.com/)
|
翻译 | [Django](http://bullteacher.com/)
|
||||||
校对 | Geequlim
|
校对 | Geequlim
|
||||||
|
|
||||||
### 图形渲染管线
|
## 图形渲染管线(Pipeline)
|
||||||
|
|
||||||
在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
|
!!! Important
|
||||||
|
|
||||||
2D坐标和像素也是不同的,2D坐标是在2D空间中的一个点的非常精确的表达,2D像素是这个点的近似值,它受到你的屏幕/窗口解析度的限制。
|
2D坐标和像素也是不同的,2D坐标是在2D空间中的一个点的非常精确的表达,2D像素是这个点的近似值,它受到你的屏幕/窗口解析度的限制。
|
||||||
|
|
||||||
图形渲染管线接收一组3D坐标,然后把它们转变为你屏幕上的有色2D像素。图形渲染管线可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行特性,当今大多数显卡都有成千上万的小处理核心,在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做 **着色器**(Shader)。
|
图形渲染管线接收一组3D坐标,然后把它们转变为你屏幕上的有色2D像素。图形渲染管线可以被划分为几个阶段,每个阶段需要把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们有一个特定的函数),它们能简单地并行执行。由于它们的并行执行特性,当今大多数显卡都有成千上万的小处理核心,在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做 **着色器**(Shader)。
|
||||||
|
|
||||||
有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了,因为它们运行在GPU上,所以它们会节约宝贵的CPU时间。OpenGL着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下一节会花更多时间研究它。
|
有些着色器允许开发者自己配置,我们可以用自己写的着色器替换默认的。这样我们就可以更细致地控制图形渲染管线中的特定部分了,因为它们运行在GPU上,所以它们会节约宝贵的CPU时间。OpenGL着色器是用**OpenGL着色器语言**(OpenGL Shading Language, GLSL)写成的,我们在下一节会花更多时间研究它。
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
如你所见,图形渲染管线包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释渲染管线的每个部分,从而使你对图形渲染管线的工作方式有个大概了解。
|
如你所见,图形渲染管线包含很多部分,每个都是将你的顶点数据转变为最后渲染出来的像素这个大过程中的一个特定阶段。我们会概括性地解释渲染管线的每个部分,从而使你对图形渲染管线的工作方式有个大概了解。
|
||||||
|
|
||||||
我们以数组的形式传递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的线性关系,只不过二维坐标系是一个扁扁的平面空间,而一条直线是一个很瘦的长长的空间。
|
①译注:当我们谈论一个“位置”的时候,它代表在一个“空间”中所处地点的这个特殊属性;同时“空间”代表着任何一种坐标系,比如x、y、z三维坐标系,x、y二维坐标系,或者一条直线上的x和y的线性关系,只不过二维坐标系是一个扁扁的平面空间,而一条直线是一个很瘦的长长的空间。
|
||||||
|
|
||||||
@@ -32,34 +32,34 @@
|
|||||||
|
|
||||||
为了让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)拥有把给定基本图形**细分**为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
|
**细分着色器**(Tessellation Shaders)拥有把给定基本图形**细分**为更多小基本图形的能力。这样我们就能在物体更接近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
|
||||||
|
|
||||||
细分着色器的输出会进入**光栅化**(Rasterization也译为像素化)阶段,这里它会把基本图形映射为屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前,会执行**裁切**(clipping)。裁切会丢弃超出你的视图以外的那些像素,来提升执行效率。
|
细分着色器的输出会进入**光栅化**(Rasterization也译为像素化)阶段,这里它会把基本图形映射为屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前,会执行**裁切**(Clipping)。裁切会丢弃超出你的视图以外的那些像素,来提升执行效率。
|
||||||
|
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
OpenGL中的一个fragment是OpenGL渲染一个独立像素所需的所有数据。
|
OpenGL中的一个fragment是OpenGL渲染一个独立像素所需的所有数据。
|
||||||
|
|
||||||
**片段着色器**的主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。通常,片段着色器包含用来计算像素最终颜色的3D场景的一些数据(比如光照、阴影、光的颜色等等)。
|
**片段着色器**的主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。通常,片段着色器包含用来计算像素最终颜色的3D场景的一些数据(比如光照、阴影、光的颜色等等)。
|
||||||
|
|
||||||
在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**(Blending)阶段。这个阶段检测像素的相应的深度(和Stencil)值(后面会讲),使用这些,来检查这个像素是否在另一个物体的前面或后面,如此做到相应取舍。这个阶段也会检查**alpha值**(alpha值是一个物体的透明度值)和物体之间的**混合**(Blend)。所以,即使在片段着色器中计算出来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能完全不同。
|
在所有相应颜色值确定以后,最终它会传到另一个阶段,我们叫做**alpha测试**和**混合**(Blending)阶段。这个阶段检测像素的相应的深度(和Stencil)值(后面会讲),使用这些,来检查这个像素是否在另一个物体的前面或后面,如此做到相应取舍。这个阶段也会检查**alpha值**(alpha值是一个物体的透明度值)和物体之间的**混合**(Blend)。所以,即使在片段着色器中计算出来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能完全不同。
|
||||||
|
|
||||||
正如你所见的那样,图形渲染管线非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。
|
正如你所见的那样,图形渲染管线非常复杂,它包含很多要配置的部分。然而,对于大多数场合,我们必须做的只是顶点和片段着色器。几何着色器和细分着色器是可选的,通常使用默认的着色器就行了。
|
||||||
|
|
||||||
在现代OpenGL中,我们**必须**定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。出于这个原因,开始学习现代OpenGL的时候非常困难,因为在你能够渲染自己的第一个三角形之前需要一大堆知识。本节结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
|
在现代OpenGL中,我们**必须**定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。出于这个原因,开始学习现代OpenGL的时候非常困难,因为在你能够渲染自己的第一个三角形之前需要一大堆知识。本节结束就是你可以最终渲染出你的三角形的时候,你也会了解到很多图形编程知识。
|
||||||
|
|
||||||
### 顶点输入
|
## 顶点输入(Vertex Input)
|
||||||
|
|
||||||
开始绘制一些东西之前,我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是在3D坐标里(x、y和z)。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素;OpenGL只是在当它们的3个轴(x、y和z)在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**(Normalized Device Coordinates,NDC)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
|
开始绘制一些东西之前,我们必须给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是在3D坐标里(x、y和z)。OpenGL不是简单的把你所有的3D坐标变换为你屏幕上的2D像素;OpenGL只是在当它们的3个轴(x、y和z)在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做**标准化设备坐标**(Normalized Device Coordinates,NDC)会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
|
||||||
|
|
||||||
由于我们希望渲染一个三角形,我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标(也就是在OpenGL的可见区域)中。
|
由于我们希望渲染一个三角形,我们指定所有的这三个顶点都有一个3D位置。我们把它们以`GLfloat`数组的方式定义为标准化设备坐标(也就是在OpenGL的可见区域)中。
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLfloat vertices[] = {
|
GLfloat vertices[] = {
|
||||||
@@ -69,7 +69,7 @@ GLfloat vertices[] = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
由于OpenGL是在3D空间中工作的,我们渲染一个2D三角形,它的每个顶点都要有同一个z坐标0.0。在这样的方式中,三角形的每一处的深度(depth)都一样,从而使它看上去就像2D的。
|
由于OpenGL是在3D空间中工作的,我们渲染一个2D三角形,它的每个顶点都要有同一个z坐标0.0。在这样的方式中,三角形的每一处的深度(Depth)都一样,从而使它看上去就像2D的。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
@@ -77,21 +77,21 @@ GLfloat vertices[] = {
|
|||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
**标准化设备坐标**
|
**标准化设备坐标(Normalized Device Coordinates, NDC)**
|
||||||
|
|
||||||
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是**标准化设备坐标**了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
|
一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是**标准化设备坐标**了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。下面你会看到我们定义的在标准化设备坐标中的三角形(忽略z轴):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
与通常的屏幕坐标不同,y轴正方向上的点和(0,0)坐标是这个图像的中心,而不是左上角。最后你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。
|
与通常的屏幕坐标不同,y轴正方向上的点和(0,0)坐标是这个图像的中心,而不是左上角。最后你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。
|
||||||
|
|
||||||
你的标准化设备坐标接着会变换为**屏幕空间坐标**(Screen-space Coordinates),这是使用你通过`glViewport`函数提供的数据,进行**视口变换**(Viewport Transform)完成的。最后的屏幕空间坐标被变换为像素输入到片段着色器。
|
你的标准化设备坐标接着会变换为**屏幕空间坐标**(Screen-space Coordinates),这是使用你通过`glViewport`函数提供的数据,进行**视口变换**(Viewport Transform)完成的。最后的屏幕空间坐标被变换为像素输入到片段着色器。
|
||||||
|
|
||||||
有了这样的顶点数据,我们会把它作为输入数据发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。
|
有了这样的顶点数据,我们会把它作为输入数据发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建储存空间用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定如何发送给显卡。顶点着色器接着会处理我们告诉它要处理内存中的顶点的数量。
|
||||||
|
|
||||||
我们通过**顶点缓冲对象**(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时,顶点着色器几乎立即就能获得顶点,这非常快。
|
我们通过**顶点缓冲对象**(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。当数据到了显卡内存中时,顶点着色器几乎立即就能获得顶点,这非常快。
|
||||||
|
|
||||||
顶点缓冲对象(VBO)是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用`glGenBuffers`函数生成一个缓冲ID:
|
顶点缓冲对象(VBO)是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其他对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用`glGenBuffers`函数生成一个缓冲ID:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLuint VBO;
|
GLuint VBO;
|
||||||
@@ -104,13 +104,13 @@ OpenGL有很多缓冲对象类型,`GL_ARRAY_BUFFER`是其中一个顶点缓冲
|
|||||||
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
glBindBuffer(GL_ARRAY_BUFFER, VBO);
|
||||||
```
|
```
|
||||||
|
|
||||||
从这一刻起,我们使用的任何缓冲函数(在`GL_ARRAY_BUFFER`目标上)都会用来配置当前绑定的缓冲(`VBO`)。然后我们可以调用`glBufferData`函数,它会把之前定义的顶点数据复制到缓冲的内存中:
|
从这一刻起,我们使用的任何缓冲函数(在`GL_ARRAY_BUFFER`目标上)都会用来配置当前绑定的缓冲(`VBO`)。然后我们可以调用`glBufferData`函数,它会把之前定义的顶点数据复制到缓冲的内存中:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
||||||
```
|
```
|
||||||
|
|
||||||
`glBufferData`是一个用来把用户定的义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(以字节为单位);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。
|
`glBufferData`是一个用来把用户定的义数据复制到当前绑定缓冲的函数。它的第一个参数是我们希望把数据复制到上面的缓冲类型:顶点缓冲对象当前绑定到`GL_ARRAY_BUFFER`目标上。第二个参数指定我们希望传递给缓冲的数据的大小(以字节为单位);用一个简单的`sizeof`计算出顶点数据就行。第三个参数是我们希望发送的真实数据。
|
||||||
|
|
||||||
第四个参数指定了我们希望显卡如何管理给定的数据。有三种形式:
|
第四个参数指定了我们希望显卡如何管理给定的数据。有三种形式:
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
|
|||||||
|
|
||||||
现在我们把顶点数据储存在显卡的内存中,用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器,来处理这些数据。现在我们开始着手创建它们吧。
|
现在我们把顶点数据储存在显卡的内存中,用VBO顶点缓冲对象管理。下面我们会创建一个顶点和片段着色器,来处理这些数据。现在我们开始着手创建它们吧。
|
||||||
|
|
||||||
### 顶点着色器
|
## 顶点着色器
|
||||||
|
|
||||||
顶点着色器是几个着色器中的一个,它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器,如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。
|
顶点着色器是几个着色器中的一个,它是可编程的。现代OpenGL需要我们至少设置一个顶点和一个片段着色器,如果我们打算做渲染的话。我们会简要介绍一下着色器以及配置两个非常简单的着色器来绘制我们第一个三角形。下个教程里我们会更详细的讨论着色器。
|
||||||
|
|
||||||
@@ -139,21 +139,21 @@ 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)`来完成的,你后面会看到为什么我们会需要这个位置值。
|
下一步,我们在顶点着色器中声明所有的输入顶点属性,使用in关键字。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性(Attribute)。GLSL有一个向量数据类型,它包含1到4个`float`元素,包含的数量可以从它的后缀看出来。由于每个顶点都有一个3D坐标,我们就创建一个`vec3`输入变量来表示位置(Position)。我们同样也指定输入变量的位置值(Location),这是用`layout (location = 0)`来完成的,你后面会看到为什么我们会需要这个位置值。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
**向量(Vector)**
|
**向量(Vector)**
|
||||||
|
|
||||||
在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中位置和方向,二者是有用的数学属性。在GLSL中一个向量有最多4个元素,每个元素值都可以从各自代表一个空间坐标的`vec.x`、`vec.y`、`vec.z`和`vec.w`来获取到。注意`vec.w`元素不是用作表达空间中的位置的(我们处理的是3D不是4D)而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。
|
在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中位置和方向,二者是有用的数学属性。在GLSL中一个向量有最多4个元素,每个元素值都可以从各自代表一个空间坐标的`vec.x`、`vec.y`、`vec.z`和`vec.w`来获取到。注意`vec.w`元素不是用作表达空间中的位置的(我们处理的是3D不是4D)而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。
|
||||||
|
|
||||||
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的`gl_Position`变量,这个位置数据是一个`vec4`类型的。在main函数的最后,无论我们给`gl_Position`设置成什么,它都会成为着色器的输出。由于我们的输入是一个3元素的向量,我们必须把它转换为4元素。我们可以通过把`vec3`数据作为`vec4`初始化构造器的参数,同时把`w`元素设置为`1.0f`(我们会在后面解释为什么)。
|
为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的`gl_Position`变量,这个位置数据是一个`vec4`类型的。在main函数的最后,无论我们给`gl_Position`设置成什么,它都会成为着色器的输出。由于我们的输入是一个3元素的向量,我们必须把它转换为4元素。我们可以通过把`vec3`数据作为`vec4`初始化构造器的参数,同时把`w`元素设置为`1.0f`(我们会在后面解释为什么)。
|
||||||
|
|
||||||
这个顶点着色器可能是能想到的最简单的了,因为我们什么都没有处理就把输入数据输出了。在真实的应用里输入数据通常都没有在标准化设备坐标中,所以我们首先就必须把它们放进OpenGL的可视区域内。
|
这个顶点着色器可能是能想到的最简单的了,因为我们什么都没有处理就把输入数据输出了。在真实的应用里输入数据通常都没有在标准化设备坐标中,所以我们首先就必须把它们放进OpenGL的可视区域内。
|
||||||
|
|
||||||
### 编译一个着色器
|
## 编译一个着色器
|
||||||
|
|
||||||
我们已经写了一个顶点着色器源码,但是为了OpenGL能够使用它,我们必须在运行时动态编译它的源码。
|
我们已经写了一个顶点着色器源码,但是为了OpenGL能够使用它,我们必须在运行时动态编译它的源码。
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ glCompileShader(vertexShader);
|
|||||||
GLchar infoLog[512];
|
GLchar infoLog[512];
|
||||||
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
|
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
|
||||||
|
|
||||||
首先我们定义一个整型来表示是否成功编译,还需要一个储存错误消息的容器(如果有的话)。然后我们用`glGetShaderiv`检查是否编译成功了。如果编译失败,我们应该用`glGetShaderInfoLog`获取错误消息,然后打印它。
|
首先我们定义一个整型来表示是否成功编译,还需要一个储存错误消息的容器(如果有的话)。然后我们用`glGetShaderiv`检查是否编译成功了。如果编译失败,我们应该用`glGetShaderInfoLog`获取错误消息,然后打印它。
|
||||||
|
|
||||||
if(!success)
|
if(!success)
|
||||||
{
|
{
|
||||||
@@ -193,13 +193,13 @@ glCompileShader(vertexShader);
|
|||||||
|
|
||||||
如果编译的时候没有任何错误,顶点着色器就被编译成功了。
|
如果编译的时候没有任何错误,顶点着色器就被编译成功了。
|
||||||
|
|
||||||
### 片段着色器(Fragment Shader)
|
## 片段着色器(Fragment Shader)
|
||||||
|
|
||||||
片段着色器是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。
|
片段着色器是第二个也是最终我们打算创建的用于渲染三角形的着色器。片段着色器的全部,都是用来计算你的像素的最后颜色输出。为了让事情比较简单,我们的片段着色器只输出橘黄色。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)元素,通常缩写为RGBA。当定义一个OpenGL或GLSL的颜色的时候,我们就把每个颜色的强度设置在0.0到1.0之间。比如,我们设置红色为1.0f,绿色为1.0f,这样这个混合色就是黄色了。这三种颜色元素的不同调配可以生成1600万不同颜色!
|
在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)元素,通常缩写为RGBA。当定义一个OpenGL或GLSL的颜色的时候,我们就把每个颜色的强度设置在0.0到1.0之间。比如,我们设置红色为1.0f,绿色为1.0f,这样这个混合色就是黄色了。这三种颜色元素的不同调配可以生成1600万不同颜色!
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -212,7 +212,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
片段着色器只需要一个输出变量,这个变量是一个4元素表示的最终输出颜色的向量,我们可以自己计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为`color`。下面,我们简单的把一个带有alpha值为1.0(1.0代表完全不透明)的橘黄的`vec4`赋值给`color`作为输出。
|
片段着色器只需要一个输出变量,这个变量是一个4元素表示的最终输出颜色的向量,我们可以自己计算出来。我们可以用`out`关键字声明输出变量,这里我们命名为`color`。下面,我们简单的把一个带有alpha值为1.0(1.0代表完全不透明)的橘黄的`vec4`赋值给`color`作为输出。
|
||||||
|
|
||||||
编译片段着色器的过程与顶点着色器相似,尽管这次我们使用`GL_FRAGMENT_SHADER`作为着色器类型:
|
编译片段着色器的过程与顶点着色器相似,尽管这次我们使用`GL_FRAGMENT_SHADER`作为着色器类型:
|
||||||
|
|
||||||
@@ -223,11 +223,11 @@ glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
|
|||||||
glCompileShader(fragmentShader);
|
glCompileShader(fragmentShader);
|
||||||
```
|
```
|
||||||
|
|
||||||
每个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个着色器程序中(Shader Program),它是用来渲染的。
|
每个着色器现在都编译了,剩下的事情是把两个着色器对象链接到一个着色器程序中(Shader Program),它是用来渲染的。
|
||||||
|
|
||||||
### 着色器程序
|
### 着色器程序
|
||||||
|
|
||||||
着色器程序对象(Shader Program Object)是多个着色器最后链接的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象,然后当渲染物体的时候激活这个着色器程序。激活了的着色器程序的着色器,在调用渲染函数时才可用。
|
着色器程序对象(Shader Program Object)是多个着色器最后链接的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象,然后当渲染物体的时候激活这个着色器程序。激活了的着色器程序的着色器,在调用渲染函数时才可用。
|
||||||
|
|
||||||
把着色器链接为一个程序就等于把每个着色器的输出链接到下一个着色器的输入。如果你的输出和输入不匹配那么就会得到一个链接错误。
|
把着色器链接为一个程序就等于把每个着色器的输出链接到下一个着色器的输入。如果你的输出和输入不匹配那么就会得到一个链接错误。
|
||||||
|
|
||||||
@@ -264,7 +264,7 @@ glLinkProgram(shaderProgram);
|
|||||||
glUseProgram(shaderProgram);
|
glUseProgram(shaderProgram);
|
||||||
```
|
```
|
||||||
|
|
||||||
现在在`glUseProgram`函数调用之后的每个着色器和渲染函数都会用到这个程序对象(当然还有这些链接的着色器)了。
|
现在在`glUseProgram`函数调用之后的每个着色器和渲染函数都会用到这个程序对象(当然还有这些链接的着色器)了。
|
||||||
|
|
||||||
在我们把着色器对象链接到程序对象以后,不要忘记删除着色器对象;我们不再需要它们了:
|
在我们把着色器对象链接到程序对象以后,不要忘记删除着色器对象;我们不再需要它们了:
|
||||||
|
|
||||||
@@ -275,7 +275,7 @@ glDeleteShader(fragmentShader);
|
|||||||
|
|
||||||
现在,我们把输入顶点数据发送给GPU,指示GPU如何在顶点和片段着色器中处理它。还没结束,OpenGL还不知道如何解释内存中的顶点数据,以及怎样把顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。
|
现在,我们把输入顶点数据发送给GPU,指示GPU如何在顶点和片段着色器中处理它。还没结束,OpenGL还不知道如何解释内存中的顶点数据,以及怎样把顶点数据链接到顶点着色器的属性上。我们需要告诉OpenGL怎么做。
|
||||||
|
|
||||||
### 链接顶点属性
|
## 链接顶点属性
|
||||||
|
|
||||||
顶点着色器允许我们以任何我们想要的形式作为顶点属性的输入,同样它也具有很强的灵活性,这意味着我们必须手动指定我们的输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。这意味着我们必须在渲染前指定OpenGL如何解释顶点数据。
|
顶点着色器允许我们以任何我们想要的形式作为顶点属性的输入,同样它也具有很强的灵活性,这意味着我们必须手动指定我们的输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。这意味着我们必须在渲染前指定OpenGL如何解释顶点数据。
|
||||||
|
|
||||||
@@ -283,12 +283,12 @@ glDeleteShader(fragmentShader);
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
- 位置数据被储存为32-bit(4 byte)浮点值。
|
- 位置数据被储存为32-bit(4 byte)浮点值。
|
||||||
- 每个位置包含3个这样的值。
|
- 每个位置包含3个这样的值。
|
||||||
- 在这3个值之间没有空隙(或其他值)。这几个值紧密排列为一个数组。
|
- 在这3个值之间没有空隙(或其他值)。这几个值紧密排列为一个数组。
|
||||||
- 数据中第一个值是缓冲的开始位置。
|
- 数据中第一个值是缓冲的开始位置。
|
||||||
|
|
||||||
有了这些信息我们就可以告诉OpenGL如何解释顶点数据了(每一个顶点属性),我们使用`glVertexAttribPointer`这个函数:
|
有了这些信息我们就可以告诉OpenGL如何解释顶点数据了(每一个顶点属性),我们使用`glVertexAttribPointer`这个函数:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
|
||||||
@@ -297,12 +297,12 @@ glEnableVertexAttribArray(0);
|
|||||||
|
|
||||||
`glVertexAttribPointer`函数有很多参数,所以我们仔细来了解它们:
|
`glVertexAttribPointer`函数有很多参数,所以我们仔细来了解它们:
|
||||||
|
|
||||||
- 第一个参数指定我们要配置哪一个顶点属性。记住,我们在顶点着色器中使用`layout(location = 0)`定义了顶点属性——位置(Position)的位置值(Location)。这样要把顶点属性的位置值(Location)设置为0,因为我们希望把数据传递到这个顶点属性中,所以我们在这里填0。
|
- 第一个参数指定我们要配置哪一个顶点属性。记住,我们在顶点着色器中使用`layout(location = 0)`定义了顶点属性——位置(Position)的位置值(Location)。这样要把顶点属性的位置值(Location)设置为0,因为我们希望把数据传递到这个顶点属性中,所以我们在这里填0。
|
||||||
- 第二个参数指定顶点属性的大小。顶点属性是`vec3`类型,它由3个数值组成。
|
- 第二个参数指定顶点属性的大小。顶点属性是`vec3`类型,它由3个数值组成。
|
||||||
- 第三个参数指定数据的类型,这里是`GL_FLOAT`(GLSL中`vec*`是由浮点数组成的)。
|
- 第三个参数指定数据的类型,这里是`GL_FLOAT`(GLSL中`vec*`是由浮点数组成的)。
|
||||||
- 下个参数定义我们是否希望数据被标准化。如果我们设置为`GL_TRUE`,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为`GL_FALSE`。
|
- 下个参数定义我们是否希望数据被标准化。如果我们设置为`GL_TRUE`,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为`GL_FALSE`。
|
||||||
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性之间间隔有多少。由于下个位置数据在3个`GLfloat`后面的位置,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。每当我们有更多的顶点属性,我们就必须小心地定义每个顶点属性之间的空间,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
|
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性之间间隔有多少。由于下个位置数据在3个`GLfloat`后面的位置,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。每当我们有更多的顶点属性,我们就必须小心地定义每个顶点属性之间的空间,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
|
||||||
- 最后一个参数有古怪的`GLvoid*`的强制类型转换。它表示我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始,所以这里是0。我们会在后面详细解释这个参数。
|
- 最后一个参数有奇怪的`GLvoid*`的强制类型转换。它表示我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始,所以这里是0。我们会在后面详细解释这个参数。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
@@ -323,11 +323,11 @@ glUseProgram(shaderProgram);
|
|||||||
someOpenGLFunctionThatDrawsOurTriangle();
|
someOpenGLFunctionThatDrawsOurTriangle();
|
||||||
```
|
```
|
||||||
|
|
||||||
我们绘制一个物体的时候必须重复这件事。这看起来也不多,但是如果有超过5个顶点属性,100多个不同物体(这并不罕见)呢。绑定合适的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有的配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态?
|
我们绘制一个物体的时候必须重复这件事。这看起来也不多,但是如果有超过5个顶点属性,100多个不同物体呢(这其实并不罕见)。绑定合适的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有的配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态?
|
||||||
|
|
||||||
### 顶点数组对象(Vertex Array Object,VAO)
|
### 顶点数组对象(Vertex Array Object,VAO)
|
||||||
|
|
||||||
**顶点数组对象(Vertex Array Object, VAO)**可以像顶点缓冲对象一样绑定,任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处,当配置顶点属性指针时,你只用做一次,每次绘制一个物体的时候,我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。
|
**顶点数组对象(Vertex Array Object, VAO)**可以像顶点缓冲对象一样绑定,任何随后的顶点属性调用都会储存在这个VAO中。这有一个好处,当配置顶点属性指针时,你只用做一次,每次绘制一个物体的时候,我们绑定相应VAO就行了。切换不同顶点数据和属性配置就像绑定一个不同的VAO一样简单。所有状态我们都放到了VAO里。
|
||||||
|
|
||||||
!!! Attention
|
!!! Attention
|
||||||
|
|
||||||
@@ -382,11 +382,11 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||
通常情况下当我们配置好它们以后要解绑OpenGL对象,这样我们才不会在某处错误地配置它们。
|
通常情况下当我们配置好它们以后要解绑OpenGL对象,这样我们才不会在某处错误地配置它们。
|
||||||
|
|
||||||
就是现在!前面做的一切都是等待这一刻,我们已经把我们的顶点属性配置和打算使用的VBO储存到一个VAO中。一般当你有多个物体打算绘制时,你首先要生成/配置所有的VAO(它需要VBO和属性指针),然后储存它们准备后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
|
就是现在!前面做的一切都是等待这一刻,我们已经把我们的顶点属性配置和打算使用的VBO储存到一个VAO中。一般当你有多个物体打算绘制时,你首先要生成/配置所有的VAO(它需要VBO和属性指针),然后储存它们准备后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
|
||||||
|
|
||||||
### 我们一直期待的三角形
|
### 我们一直期待的三角形
|
||||||
|
|
||||||
OpenGL的`glDrawArrays`函数为我们提供了绘制物体的能力,它使用当前激活的着色器、前面定义的顶点属性配置和VBO的顶点数据(通过VAO间接绑定)来绘制基本图形。
|
OpenGL的`glDrawArrays`函数为我们提供了绘制物体的能力,它使用当前激活的着色器、前面定义的顶点属性配置和VBO的顶点数据(通过VAO间接绑定)来绘制基本图形。
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
glUseProgram(shaderProgram);
|
glUseProgram(shaderProgram);
|
||||||
@@ -395,7 +395,7 @@ glDrawArrays(GL_TRIANGLES, 0, 3);
|
|||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
```
|
```
|
||||||
|
|
||||||
`glDrawArrays`函数第一个参数是我们打算绘制的OpenGL基本图形的类型。由于我们在一开始时说过,我们希望绘制三角形,我们传递`GL_TRIANGLES`给它。第二个参数定义了我们打算绘制的那个顶点数组的起始位置的索引;我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据渲染一个三角形,它只有3个顶点)。
|
`glDrawArrays`函数第一个参数是我们打算绘制的OpenGL基本图形的类型。由于我们在一开始时说过,我们希望绘制三角形,我们传递`GL_TRIANGLES`给它。第二个参数定义了我们打算绘制的那个顶点数组的起始位置的索引;我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据渲染一个三角形,它只有3个顶点)。
|
||||||
|
|
||||||
现在尝试编译代码,如果弹出了任何错误,回头检查你的代码。如果你编译通过了,你应该看到下面的结果:
|
现在尝试编译代码,如果弹出了任何错误,回头检查你的代码。如果你编译通过了,你应该看到下面的结果:
|
||||||
|
|
||||||
@@ -405,9 +405,9 @@ glBindVertexArray(0);
|
|||||||
|
|
||||||
如果你的输出和这个不一样,你可能做错了什么,去看源码,看看是否遗漏了什么东西或者在评论部分提问。
|
如果你的输出和这个不一样,你可能做错了什么,去看源码,看看是否遗漏了什么东西或者在评论部分提问。
|
||||||
|
|
||||||
### 索引缓冲对象(Element Buffer Objects,EBO)
|
## 索引缓冲对象(Element Buffer Objects,EBO)
|
||||||
|
|
||||||
这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象简称EBO(或IBO)。解释索引缓冲对象的工作方式最好是举例子:假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形(OpenGL主要就是绘制三角形)。这会生成下面的顶点的集合:
|
这是我们最后一件在渲染顶点这个问题上要讨论的事——索引缓冲对象简称EBO(或IBO)。解释索引缓冲对象的工作方式最好是举例子:假设我们不再绘制一个三角形而是矩形。我们就可以绘制两个三角形来组成一个矩形(OpenGL主要就是绘制三角形)。这会生成下面的顶点的集合:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLfloat vertices[] = {
|
GLfloat vertices[] = {
|
||||||
@@ -426,7 +426,7 @@ GLfloat vertices[] = {
|
|||||||
|
|
||||||
就像你所看到的那样,有几个顶点叠加了。我们指定右下角和左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有超过1000个三角的模型这个问题会更糟糕,这会产生一大堆浪费。最好的解决方案就是每个顶点只储存一次,当我们打算绘制这些顶点的时候只调用顶点的索引。这种情况我们只要储存4个顶点就能绘制矩形了,我们只要指定我们打算绘制的那个顶点的索引就行了。如果OpenGL提供这个功能就好了,对吧?
|
就像你所看到的那样,有几个顶点叠加了。我们指定右下角和左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有超过1000个三角的模型这个问题会更糟糕,这会产生一大堆浪费。最好的解决方案就是每个顶点只储存一次,当我们打算绘制这些顶点的时候只调用顶点的索引。这种情况我们只要储存4个顶点就能绘制矩形了,我们只要指定我们打算绘制的那个顶点的索引就行了。如果OpenGL提供这个功能就好了,对吧?
|
||||||
|
|
||||||
很幸运,索引缓冲的工作方式正是这样的。一个EBO是一个像顶点缓冲对象(VBO)一样的缓冲,它专门储存索引,OpenGL调用这些顶点的索引来绘制。索引绘制正是这个问题的解决方案。我们先要定义(独一无二的)顶点,和绘制出矩形的索引:
|
很幸运,索引缓冲的工作方式正是这样的。一个EBO是一个像顶点缓冲对象(VBO)一样的缓冲,它专门储存索引,OpenGL调用这些顶点的索引来绘制。索引绘制正是这个问题的解决方案。我们先要定义(独一无二的)顶点,和绘制出矩形的索引:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLfloat vertices[] = {
|
GLfloat vertices[] = {
|
||||||
@@ -465,7 +465,7 @@ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
|
|||||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||||
```
|
```
|
||||||
|
|
||||||
第一个参数指定了我们绘制的模式,这个和`glDrawArrays`的一样。第二个参数是我们打算绘制顶点的次数。我们填6,说明我们总共想绘制6个顶点。第三个参数是索引的类型,这里是`GL_UNSIGNED_INT`。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这只是当你不是在使用索引缓冲对象的时候),但是我们只打算在这里填写0。
|
第一个参数指定了我们绘制的模式,这个和`glDrawArrays`的一样。第二个参数是我们打算绘制顶点的次数。我们填6,说明我们总共想绘制6个顶点。第三个参数是索引的类型,这里是`GL_UNSIGNED_INT`。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这只是当你不是在使用索引缓冲对象的时候),但是我们只打算在这里填写0。
|
||||||
|
|
||||||
`glDrawElements`函数从当前绑定到`GL_ELEMENT_ARRAY_BUFFER`目标的EBO获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象仍可以保存索引缓冲对象的绑定状态。VAO绑定之后可以索引缓冲对象,EBO就成为了VAO的索引缓冲对象。再次绑定VAO的同时也会自动绑定EBO。
|
`glDrawElements`函数从当前绑定到`GL_ELEMENT_ARRAY_BUFFER`目标的EBO获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象仍可以保存索引缓冲对象的绑定状态。VAO绑定之后可以索引缓冲对象,EBO就成为了VAO的索引缓冲对象。再次绑定VAO的同时也会自动绑定EBO。
|
||||||
|
|
||||||
@@ -494,7 +494,7 @@ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices),indices, GL_STATIC_DRAW);
|
|||||||
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
|
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
|
||||||
glEnableVertexAttribArray(0);
|
glEnableVertexAttribArray(0);
|
||||||
|
|
||||||
// 4. 解绑VAO,不解绑EBO(译注:解绑缓冲相当于没有绑定缓冲,可以在解绑VAO之后解绑缓冲)
|
// 4. 解绑VAO,不解绑EBO(译注:解绑缓冲相当于没有绑定缓冲,可以在解绑VAO之后解绑缓冲)
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|
||||||
[...]
|
[...]
|
||||||
@@ -507,7 +507,7 @@ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
|
|||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
```
|
```
|
||||||
|
|
||||||
运行程序会获得下面这样的图片的结果。左侧图片看起来很熟悉,而右侧的则是使用线框模式(Wireframe Mode)绘制的。线框矩形可以显示出矩形的确是由两个三角形组成的。
|
运行程序会获得下面这样的图片的结果。左侧图片看起来很熟悉,而右侧的则是使用线框模式(Wireframe Mode)绘制的。线框矩形可以显示出矩形的确是由两个三角形组成的。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -532,5 +532,5 @@ glBindVertexArray(0);
|
|||||||
为了更好的理解讨论的那些概念最好做点练习。建议在继续下面的主题之前做完这些练习,确保你对这些有比较好的理解。
|
为了更好的理解讨论的那些概念最好做点练习。建议在继续下面的主题之前做完这些练习,确保你对这些有比较好的理解。
|
||||||
|
|
||||||
- 尝试使用`glDrawArrays`以在你的数据中添加更多顶点的方式,绘制两个彼此相连的三角形:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise1)
|
- 尝试使用`glDrawArrays`以在你的数据中添加更多顶点的方式,绘制两个彼此相连的三角形:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise1)
|
||||||
- 现在,使用不同的VAO(和VBO)创建同样的2个三角形,每个三角形的数据要不同(提示:创建2个顶点数据数组,而不是1个):[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise2)
|
- 现在,使用不同的VAO(和VBO)创建同样的2个三角形,每个三角形的数据要不同(提示:创建2个顶点数据数组,而不是1个):[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise2)
|
||||||
- 创建连个着色器程序(Shader Program),第二个程序使用不同的片段着色器,它输出黄色;绘制这两个三角形,其中一个输出为黄色:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise3)
|
- 创建连个着色器程序(Shader Program),第二个程序使用不同的片段着色器,它输出黄色;绘制这两个三角形,其中一个输出为黄色:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/hello-triangle-exercise3)
|
@@ -1,4 +1,4 @@
|
|||||||
## 着色器(Shaders)
|
# 着色器(Shaders)
|
||||||
|
|
||||||
原文 | [Shaders](http://learnopengl.com/#!Getting-started/Shaders)
|
原文 | [Shaders](http://learnopengl.com/#!Getting-started/Shaders)
|
||||||
---|---
|
---|---
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
|
|
||||||
前面的教程里我们简要地触及了一点着色器的皮毛。了解了如何恰当地使用它们。现在我们会用一种更加通用的方式详细解释着色器,特别是OpenGL着色器语言。
|
前面的教程里我们简要地触及了一点着色器的皮毛。了解了如何恰当地使用它们。现在我们会用一种更加通用的方式详细解释着色器,特别是OpenGL着色器语言。
|
||||||
|
|
||||||
### GLSL
|
## GLSL
|
||||||
|
|
||||||
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含瞄准向量和矩阵操纵的有用特性。
|
着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含针对向量和矩阵操作的有用特性。
|
||||||
|
|
||||||
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口都是main函数,在这里我们处理所有输入变量,用输出变量输出结果。如果你不知道什么是uniform也不用担心,我们后面会进行讲解。
|
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和`main`函数。每个着色器的入口都是`main`函数,在这里我们处理所有输入变量,用输出变量输出结果。如果你不知道什么是uniform也不用担心,我们后面会进行讲解。
|
||||||
|
|
||||||
一个典型的着色器有下面的结构:
|
一个典型的着色器有下面的结构:
|
||||||
|
|
||||||
@@ -37,19 +37,23 @@ int main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
当我们谈论特别是谈到顶点着色器的时候,每个输入变量也叫顶点属性(vertex attribute)。能声明多少个顶点属性是由硬件决定的。OpenGL确保至少有16个包含4个元素的顶点属性可用,但是有些硬件或许可用更多,你可以查询GL_MAX_VERTEX_ATTRIBS来获取这个数目。
|
当我们谈论特别是谈到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。能声明多少个顶点属性是由硬件决定的。OpenGL确保至少有16个包含4个元素的顶点属性可用,但是有些硬件或许可用更多,你可以查询`GL_MAX_VERTEX_ATTRIB`S来获取这个数目。
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLint nrAttributes;
|
GLint nrAttributes;
|
||||||
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
|
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
|
||||||
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
|
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
|
||||||
```
|
```
|
||||||
### 数据类型
|
|
||||||
|
|
||||||
GLSL有像其他编程语言相似的数据类型。GLSL有C风格的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,教程中我们会使用很多,它们是向量(vector)和矩阵(matrix),我们会在后面的教程里讨论他们。
|
通常情况下它会返回至少16个,大部分情况下是够用了。
|
||||||
|
|
||||||
#### 向量
|
## 数据类型
|
||||||
|
|
||||||
GLSL中的向量是前面提到的一个有1、2、3或4个元素的基本类型容器。它们可以是下面的形式(n代表元素数量):
|
GLSL有像其他编程语言相似的数据类型。GLSL有C风格的默认基础数据类型:`int`、`float`、`double`、`uint`和`bool`。GLSL也有两种容器类型,教程中我们会使用很多,它们是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。
|
||||||
|
|
||||||
|
## 向量(Vector)
|
||||||
|
|
||||||
|
GLSL中的向量可以包含有1、2、3或者4个分量,分量类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表元素数量):
|
||||||
|
|
||||||
类型|含义
|
类型|含义
|
||||||
---|---
|
---|---
|
||||||
@@ -61,9 +65,9 @@ GLSL中的向量是前面提到的一个有1、2、3或4个元素的基本类型
|
|||||||
|
|
||||||
大多数时候我们使用vecn,因为float足够满足大多数要求。
|
大多数时候我们使用vecn,因为float足够满足大多数要求。
|
||||||
|
|
||||||
一个向量的元素可以通过vec.x这种方式获取,这里x是指这个向量的第一个元素。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4号元素。GLSL也允许你使用rgba来获取颜色的元素,或是stpq获取纹理坐标元素。
|
一个向量的元素可以通过`vec.x`这种方式获取,这里`x`是指这个向量的第一个元素。你可以分别使用`.x`、`.y`、`.z`和`.w`来获取它们的第1、2、3、4号元素。GLSL也允许你使用**rgba**来获取颜色的元素,或是**stpq**获取纹理坐标元素。
|
||||||
|
|
||||||
向量的数据类型也允许一些有趣而灵活的元素选择方式,叫做swizzling(混合)。swizzling是这样的语法:
|
向量的数据类型也允许一些有趣而灵活的元素选择方式,叫做重组(Swizzling)。重组允许这样的语法:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
vec2 someVec;
|
vec2 someVec;
|
||||||
@@ -72,7 +76,7 @@ vec3 anotherVec = differentVec.zyw;
|
|||||||
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
|
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
|
||||||
```
|
```
|
||||||
|
|
||||||
你可以使用上面任何4个字母组合来创建一个新的和原来向量一样长的向量(但4个元素需要是同一种类型);不允许在一个vec2向量中去获取.z元素。我们可以把一个向量作为一个参数传给不同的向量构造函数,以减少参数需求的数量:
|
你可以使用上面任何4个字母组合来创建一个新的和原来向量一样长的向量(但4个元素需要是同一种类型);不允许在一个vec2向量中去获取.z元素。我们可以把一个向量作为一个参数传给不同的向量构造函数,以减少参数需求的数量:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
vec2 vect = vec2(0.5f, 0.7f);
|
vec2 vect = vec2(0.5f, 0.7f);
|
||||||
@@ -80,43 +84,43 @@ vec4 result = vec4(vect, 0.0f, 0.0f);
|
|||||||
vec4 otherResult = vec4(result.xyz, 1.0f);
|
vec4 otherResult = vec4(result.xyz, 1.0f);
|
||||||
```
|
```
|
||||||
|
|
||||||
向量是一种灵活的数据类型,我们可以把用在所有输入和输出上。学完教程你会看到足够多如何创造性地管理向量的例子。
|
向量是一种灵活的数据类型,我们可以把用在所有输入和输出上。学完教程你会看到很多如何创造性地管理向量的例子。
|
||||||
|
|
||||||
#### 输入与输出(in vs out)
|
## 输入与输出(in vs out)
|
||||||
|
|
||||||
着色器是各自独立的小程序,但是它们都是一个整体的局部,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字来实现这个目的。每个着色器使用这些关键字定义输入和输出,无论在哪儿,一个输出变量就能与一个下一个阶段的输入变量相匹配。他们在顶点和片段着色器之间有点不同。
|
着色器是各自独立的小程序,但是它们都是一个整体的局部,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了`in`和`out`关键字来实现这个目的。每个着色器使用这些关键字定义输入和输出,无论在哪儿,一个输出变量就能与一个下一个阶段的输入变量相匹配。他们在顶点和片段着色器之间有点不同。
|
||||||
|
|
||||||
顶点着色器应该接收的输入是一种特有形式,否则就会效率低下。顶点着色器的输入是特殊的,它所接受的是从顶点数据直接输入的。为了定义顶点数据被如何组织,我们使用`location`元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过`layout (location = 0)`。顶点着色器需要为它的输入提供一个额外的layout定义,这样我们才能把它链接到顶点数据。
|
顶点着色器应该接收的输入是一种特有形式,否则就会效率低下。顶点着色器的输入是特殊的,它所接受的是从顶点数据直接输入的。为了定义顶点数据被如何组织,我们使用`location`元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过`layout (location = 0)`。顶点着色器需要为它的输入提供一个额外的`layout`定义,这样我们才能把它链接到顶点数据。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
也可以移除`layout (location = 0)`,通过在OpenGL代码中使用`glGetAttribLocation`请求属性地址(location),但是我更喜欢在着色器中设置它们,理解容易而且节省时间。
|
也可以移除`layout (location = 0)`,通过在OpenGL代码中使用`glGetAttribLocation`请求属性地址(Location),但是我更喜欢在着色器中设置它们,理解容易而且节省时间。
|
||||||
|
|
||||||
另一个例外是片段着色器需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
|
另一个例外是片段着色器需要一个`vec4`颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
|
||||||
|
|
||||||
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须**在发送方着色器中声明一个输出,在接收方着色器中声明一个同名输入**。当名字和类型都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序(program)对象时完成的)。为了展示这是这么工作的,我们会改变前面教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
|
所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须**在发送方着色器中声明一个输出,在接收方着色器中声明一个同名输入**。当名字和类型都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序(Program)对象时完成的)。为了展示这是这么工作的,我们会改变前面教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
|
||||||
|
|
||||||
##### 顶点着色器
|
#### 顶点着色器
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
layout (location = 0) in vec3 position; // 位置变量的属性position为0 attribute position 0
|
layout (location = 0) in vec3 position; // 位置变量的属性为0
|
||||||
|
|
||||||
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
|
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
gl_Position = vec4(position, 1.0); // 把一个vec3作为vec4的构造器的参数 to vec4's constructor
|
gl_Position = vec4(position, 1.0); // 把一个vec3作为vec4的构造器的参数
|
||||||
vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出颜色设置为暗红色 to a dark-red color
|
vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出颜色设置为暗红色
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
##### 片段着色器
|
#### 片段着色器
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
in vec4 vertexColor; // 和顶点着色器的vertexColor变量类型相同、名称相同
|
in vec4 vertexColor; // 和顶点着色器的vertexColor变量类型相同、名称相同
|
||||||
|
|
||||||
out vec4 color; //片段着色器输出的变量名可以任意命名,类型必须是vec4
|
out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
@@ -124,15 +128,15 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,在片段着色器声明了一个一样的vertexColor。由于它们**类型相同并且名字也相同**,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。因为我们在顶点着色器中设置的颜色是深红色的,片段着色器输出的结果也是深红色的。下面的图片展示了输出结果:
|
你可以看到我们在顶点着色器中声明了一个`vertexColor`变量作为`vec4`输出,在片段着色器声明了一个一样的`vertexColor`。由于它们**类型相同并且名字也相同**,片段着色器中的`vertexColor`就和顶点着色器中的`vertexColor`链接了。因为我们在顶点着色器中设置的颜色是深红色的,片段着色器输出的结果也是深红色的。下面的图片展示了输出结果:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
我们完成了从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!
|
我们完成了从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!
|
||||||
|
|
||||||
#### uniform
|
## Uniform
|
||||||
|
|
||||||
uniform是另一种从CPU应用向GPU着色器发送数据的方式,但uniform和顶点属性有点不同。首先,uniform是全局的。这里全局的意思是uniform变量必须在所有着色器程序对象中都是独一无二的,它可以在着色器程序的任何着色器任何阶段使用。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
|
uniform是另一种从CPU应用向GPU着色器发送数据的方式,但uniform和顶点属性有点不同。首先,uniform是**全局的(Global)**。这里全局的意思是uniform变量必须在所有着色器程序对象中都是独一无二的,它可以在着色器程序的任何着色器任何阶段使用。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
|
||||||
|
|
||||||
我们可以简单地通过在片段着色器中设置uniform关键字接类型和变量名来声明一个GLSL的uniform。之后,我们可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色:
|
我们可以简单地通过在片段着色器中设置uniform关键字接类型和变量名来声明一个GLSL的uniform。之后,我们可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色:
|
||||||
|
|
||||||
@@ -148,7 +152,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们在片段着色器中声明了一个uniform vec4 ourColor,并把片段着色器的输出颜色设置为uniform值。因为uniform是全局变量,我们我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以不用在那里定义它。
|
我们在片段着色器中声明了一个uniform vec4的`ourColor`,并把片段着色器的输出颜色设置为uniform值。因为uniform是全局变量,我们我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform所以不用在那里定义它。
|
||||||
|
|
||||||
!!! Attention
|
!!! Attention
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
|
|||||||
|
|
||||||
首先我们通过`glfwGetTime()`获取运行的秒数。然后我们使用余弦函数在0.0到-1.0之间改变颜色,最后储存到`greenValue`里。
|
首先我们通过`glfwGetTime()`获取运行的秒数。然后我们使用余弦函数在0.0到-1.0之间改变颜色,最后储存到`greenValue`里。
|
||||||
|
|
||||||
接着,我们用`glGetUniformLocation`请求`uniform ourColor`的地址。我们为请求函数提供着色器程序和uniform的名字(这是我们希望获得的地址的来源)。如果`glGetUniformLocation`返回`-1`就代表没有找到这个地址。最后,我们可以通过`glUniform4f`函数设置uniform值。注意,查询uniform地址不需要在之前使用着色器程序(use shader program),但是更新一个unform之前必须不许使用程序(调用`glUseProgram`),因为它是在当前激活的着色器程序中设置unform的。
|
接着,我们用`glGetUniformLocation`请求`uniform ourColor`的地址。我们为请求函数提供着色器程序和uniform的名字(这是我们希望获得的地址的来源)。如果`glGetUniformLocation`返回`-1`就代表没有找到这个地址。最后,我们可以通过`glUniform4f`函数设置uniform值。注意,查询uniform地址不需要在之前使用着色器程序,但是更新一个unform之前**必须**使用程序(调用`glUseProgram`),因为它是在当前激活的着色器程序中设置unform的。
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
@@ -180,7 +184,7 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
|
|||||||
3f| 函数需要3个float作为它的值
|
3f| 函数需要3个float作为它的值
|
||||||
fv| 函数需要一个float向量/数组作为它的值
|
fv| 函数需要一个float向量/数组作为它的值
|
||||||
|
|
||||||
每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载的函数。在我们的例子里,我们使用uniform的4float版,所以我们通过`glUniform4f`传递我们的数据(注意,我们也可以使用fv版本)。
|
每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载的函数。在我们的例子里,我们使用uniform的4float版,所以我们通过`glUniform4f`传递我们的数据(注意,我们也可以使用fv版本)。
|
||||||
|
|
||||||
现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一帧更新这个uniform,否则三角形就不会改变颜色。下面我们就计算greenValue然后每个渲染迭代都更新这个uniform:
|
现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一帧更新这个uniform,否则三角形就不会改变颜色。下面我们就计算greenValue然后每个渲染迭代都更新这个uniform:
|
||||||
|
|
||||||
@@ -219,9 +223,9 @@ while(!glfwWindowShouldClose(window))
|
|||||||
|
|
||||||
就像你所看到的那样,uniform是个设置属性的很有用的工具,它可以在渲染循环中改变,也可以在你的应用和着色器之间进行数据交互,但假如我们打算为每个顶点设置一个颜色的时候该怎么办?这种情况下,我们就不得不声明和顶点数目一样多的uniform了。在顶点属性问题上一个更好的解决方案一定要能包含足够多的数据,这是我们接下来要讲的内容。
|
就像你所看到的那样,uniform是个设置属性的很有用的工具,它可以在渲染循环中改变,也可以在你的应用和着色器之间进行数据交互,但假如我们打算为每个顶点设置一个颜色的时候该怎么办?这种情况下,我们就不得不声明和顶点数目一样多的uniform了。在顶点属性问题上一个更好的解决方案一定要能包含足够多的数据,这是我们接下来要讲的内容。
|
||||||
|
|
||||||
##### 更多属性
|
## 更多属性
|
||||||
|
|
||||||
前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据表示为3个float的顶点数组。我们为三角形的每个角分别指定为红色、绿色和蓝色:
|
前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据表示为3个float的**顶点数组(Vertex Array)**。我们为三角形的每个角分别指定为红色、绿色和蓝色:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
GLfloat vertices[] = {
|
GLfloat vertices[] = {
|
||||||
@@ -232,14 +236,14 @@ GLfloat vertices[] = {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
由于我们现在发送到顶点着色器的数据更多了,有必要调整顶点着色器,使它能够把颜色值作为一个顶点属性输入。需要注意的是我们用`layout`标识符来吧颜色属性的`location`设置为1:
|
由于我们现在发送到顶点着色器的数据更多了,有必要调整顶点着色器,使它能够把颜色值作为一个顶点属性输入。需要注意的是我们用`layout`标识符来吧`color`属性的`location`设置为1:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
layout (location = 0) in vec3 position; // 位置变量的属性position为 0
|
layout (location = 0) in vec3 position; // 位置变量的属性position为 0
|
||||||
layout (location = 1) in vec3 color; // 颜色变量的属性position为 1
|
layout (location = 1) in vec3 color; // 颜色变量的属性position为 1
|
||||||
|
|
||||||
out vec3 ourColor; // 向fragment shader输出一个颜色
|
out vec3 ourColor; // 向片段着色器输出一个颜色
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
@@ -248,7 +252,7 @@ void main()
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
由于我们不再使用uniform来传递fragment的颜色了,现在使用的`ourColor`输出变量要求必须也去改变片段着色器:
|
由于我们不再使用uniform来传递片段的颜色了,现在使用的`ourColor`输出变量要求必须也去改变片段着色器:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
#version 330 core
|
#version 330 core
|
||||||
@@ -261,9 +265,10 @@ void main()
|
|||||||
```
|
```
|
||||||
|
|
||||||
因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
|
因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
我们可以使用`glVertexAttribPointer`函数更新了顶点格式,告知它当前的是哪个layout:
|
知道了当前使用的layout,我们就可以使用`glVertexAttribPointer`函数更新顶点格式,
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
// 顶点属性
|
// 顶点属性
|
||||||
@@ -276,29 +281,28 @@ glEnableVertexAttribArray(1);
|
|||||||
|
|
||||||
`glVertexAttribPointer`函数的前几个参数比较明了。这次我们配置属性location为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。
|
`glVertexAttribPointer`函数的前几个参数比较明了。这次我们配置属性location为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。
|
||||||
|
|
||||||
由于我们现在有了两个顶点属性,我们不得不重新计算stride(步长)值。为获得数据队列中下一个属性值(比如位置向量的下个x元素)我们必须向右移动6个float,其中3个是位置值,另外三个是颜色值。这给了我们6个stride(步长)的大小,每个stride(步长)是float的字节数(=24字节)。
|
由于我们现在有了两个顶点属性,我们不得不重新计算步长值(Stride)。为获得数据队列中下一个属性值(比如位置向量的下个x元素)我们必须向右移动6个float,其中3个是位置值,另外三个是颜色值。这给了我们6个步长的大小,每个步长都是float的字节数(=24字节)。
|
||||||
|
|
||||||
同样,这次我们必须指定一个偏移量(offset)。对于每个顶点来说,位置(position)顶点属性是先声明的,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3*sizeof(GLfloat),用字节来计算就是12字节。
|
同样,这次我们必须指定一个偏移量(Offset)。对于每个顶点来说,位置(Position)顶点属性是先声明的,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是`3*sizeof(GLfloat)`,用字节来计算就是12字节。
|
||||||
|
|
||||||
运行应用你会看到如下结果:
|
运行应用你会看到如下结果:
|
||||||

|

|
||||||
|
|
||||||
如果你有困惑,可以[在这里获得源码](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-interpolated)。
|
如果你有困惑,可以[在这里获得源码](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-interpolated)。
|
||||||
|
|
||||||
这个图片可能不是你所期望的那种,因为我们只提供3个颜色,而不是我们现在看到的大调色板。这是所谓片段着色器进行像素插值(fragment interpolation)的结果。当渲染一个三角形在(rasterization 也译为光栅化)像素化阶段通常生成比原来的顶点更多的像素。像素器就会基于每个像素在三角形的所处相对位置决定像素的位置。
|
这个图片可能不是你所期望的那种,因为我们只提供3个颜色,而不是我们现在看到的大调色板。这是所谓片段着色器进行**片段插值(Fragment Interpolation)**的结果。当渲染一个三角形在像素化(Rasterization 也译为光栅化)阶段通常生成比原来的顶点更多的像素。像素器就会基于每个像素在三角形的所处相对位置决定像素的位置。
|
||||||
|
|
||||||
基于这些位置,它插入所有片段着色器的输入变量。比如说,我们有一个线段,上面的那个点是绿色的,下面的点是蓝色的。如果一个片段着色器正在处理的那个像素(实际上就是像素)是在线段的70%的位置,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝+70%绿。
|
基于这些位置,它**插入(Interpolate)**所有片段着色器的输入变量。比如说,我们有一个线段,上面的那个点是绿色的,下面的点是蓝色的。如果一个片段着色器正在处理的那个片段(实际上就是像素)是在线段的70%的位置,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝+70%绿。
|
||||||
|
|
||||||
这正是这个三角形里发生的事。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50,000左右的像素,片段着色器为这些像素进行插值。如果你仔细看这些颜色,你会发现其中的奥秘:红到紫再到蓝。像素插值会应用到所有片段着色器的输入属性上。
|
这正是这个三角形里发生的事。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50,000左右的像素,片段着色器为这些像素进行插值。如果你仔细看这些颜色,你会发现其中的奥秘:红到紫再到蓝。像素插值会应用到所有片段着色器的输入属性上。
|
||||||
|
|
||||||
### 我们自己的着色器类
|
## 我们自己的着色器类
|
||||||
|
|
||||||
编写、编译、管理着色器是件麻烦事。在着色器的最后主题里,我们会写一个类来让我们的生活轻松一点,这个类从硬盘读着色器,然后编译和链接它们,对它们进行错误检测,这就变得很好用了。这也会给你一些关于如何把我们目前所学的知识封装到一个抽象的对象里的灵感。
|
编写、编译、管理着色器是件麻烦事。在着色器的最后主题里,我们会写一个类来让我们的生活轻松一点,这个类从硬盘读着色器,然后编译和链接它们,对它们进行错误检测,这就变得很好用了。这也会给你一些关于如何把我们目前所学的知识封装到一个抽象的对象里的灵感。
|
||||||
|
|
||||||
我们会在头文件里创建整个类,主要为了学习,也可以方便移植。我们先来添加必要的include,定义类结构:
|
我们会在头文件里创建整个类,主要为了学习,也可以方便移植。我们先来添加必要的include,定义类结构:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
|
|
||||||
#ifndef SHADER_H
|
#ifndef SHADER_H
|
||||||
#define SHADER_H
|
#define SHADER_H
|
||||||
|
|
||||||
@@ -323,16 +327,15 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Important
|
!!! Important
|
||||||
|
|
||||||
在上面,我们用了几个预处理指令。这些预处理指令告知你的编译器,只在没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个shader头文件,它是用来防止链接冲突的。
|
在上面,我们用了几个预处理指令(Preprocessor Directives)。这些预处理指令告知你的编译器,只在没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个shader头文件,它是用来防止链接冲突的。
|
||||||
|
|
||||||
shader类保留了shader程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,我们可以把各自的文本文件储存在硬盘上。Use函数看似平常,但是能够显示这个自造类如何让我们的生活变轻松(虽然只有一点)。
|
shader类保留了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,我们可以把各自的文本文件储存在硬盘上。`Use`函数看似平常,但是能够显示这个自造类如何让我们的生活变轻松(虽然只有一点)。
|
||||||
|
|
||||||
#### 从文件读取
|
### 从文件读取
|
||||||
|
|
||||||
我们使用C++文件流读取着色器内容,储存到几个string对象里[1]
|
我们使用C++文件流读取着色器内容,储存到几个string对象里[1]
|
||||||
|
|
||||||
@@ -344,20 +347,20 @@ Shader(const GLchar * vertexPath, const GLchar * fragmentPath)
|
|||||||
std::string fragmentCode;
|
std::string fragmentCode;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 打开文件 Open files
|
// 打开文件
|
||||||
std::ifstream vShaderFile(vertexPath);
|
std::ifstream vShaderFile(vertexPath);
|
||||||
std::ifstream fShaderFile(fragmentPath);
|
std::ifstream fShaderFile(fragmentPath);
|
||||||
|
|
||||||
std::stringstream vShaderStream, fShaderStream;
|
std::stringstream vShaderStream, fShaderStream;
|
||||||
// 读取文件缓冲到流 Read file’s buffer contents into streams
|
// 读取文件缓冲到流
|
||||||
vShaderStream << vShaderFile.rdbuf();
|
vShaderStream << vShaderFile.rdbuf();
|
||||||
fShaderStream << fShaderFile.rdbuf();
|
fShaderStream << fShaderFile.rdbuf();
|
||||||
|
|
||||||
// 关闭文件句柄 close file handlers
|
// 关闭文件句柄
|
||||||
vShaderFile.close();
|
vShaderFile.close();
|
||||||
fShaderFile.close();
|
fShaderFile.close();
|
||||||
|
|
||||||
// 将流转为GLchar数组 Convert stream into GLchar array
|
// 将流转为GLchar数组
|
||||||
vertexCode = vShaderStream.str();
|
vertexCode = vShaderStream.str();
|
||||||
fragmentCode = fShaderStream.str();
|
fragmentCode = fShaderStream.str();
|
||||||
}
|
}
|
||||||
@@ -367,7 +370,7 @@ Shader(const GLchar * vertexPath, const GLchar * fragmentPath)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
下一步,我们需要编译和链接着色器。注意,我们也要检查编译/链接是否失败,如果失败,打印编译错误,调试的时候这及其重要(这些错误日志你总会需要的):
|
下一步,我们需要编译和链接着色器。注意,我们也要检查编译/链接是否失败,如果失败,打印编译错误,调试的时候这及其重要(这些错误日志你总会需要的):
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
// 2. 编译着色器
|
// 2. 编译着色器
|
||||||
@@ -409,7 +412,9 @@ if(!success)
|
|||||||
glDeleteShader(vertex);
|
glDeleteShader(vertex);
|
||||||
glDeleteShader(fragment);
|
glDeleteShader(fragment);
|
||||||
```
|
```
|
||||||
|
|
||||||
最后我们也要实现Use函数:
|
最后我们也要实现Use函数:
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
void Use()
|
void Use()
|
||||||
{
|
{
|
||||||
@@ -430,14 +435,14 @@ while(...)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
我们把顶点和片段着色器储存为两个叫做shader.vs和shader.frag的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用.vs和.frag作为扩展名很直观。
|
我们把顶点和片段着色器储存为两个叫做`shader.vs`和`shader.frag`的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用`.vs`和`.frag`作为扩展名很直观。
|
||||||
|
|
||||||
使用新着色器类的[程序](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-using-object),[着色器类](http://learnopengl.com/code_viewer.php?type=header&code=shader),[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/basic),[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/basic)。
|
使用新着色器类的[程序](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-using-object),[着色器类](http://learnopengl.com/code_viewer.php?type=header&code=shader),[顶点着色器](http://learnopengl.com/code_viewer.php?type=vertex&code=getting-started/basic),[片段着色器](http://learnopengl.com/code_viewer.php?type=fragment&code=getting-started/basic)。
|
||||||
|
|
||||||
###练习
|
## 练习
|
||||||
|
|
||||||
修改顶点着色器让三角形上下颠倒:方案。
|
1. 修改顶点着色器让三角形上下颠倒:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise1)
|
||||||
通过使用uniform定义一个水平偏移,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧:方案。
|
2. 通过使用uniform定义一个水平偏移,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise2)
|
||||||
使用out关键字把顶点位置输出到片段着色器,把像素的颜色设置为与顶点位置相等(看看顶点位置值是如何在三角形中进行插值的)。做完这些后;尝试回答下面的问题:为什么在三角形的左下角是黑的?:方案。
|
3. 使用`out`关键字把顶点位置输出到片段着色器,把像素的颜色设置为与顶点位置相等(看看顶点位置值是如何在三角形中进行插值的)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?:[参考解答](http://learnopengl.com/code_viewer.php?code=getting-started/shaders-exercise3)
|
||||||
|
|
||||||
[1]:译者注:实际上把着色器代码保存在文件中适合学习OpenGL的时候,实际开发中最好把一个着色器直接储存为多个字符串,这样具有更高的灵活度。
|
[1]: "译者注:实际上把着色器代码保存在文件中适合学习OpenGL的时候,实际开发中最好把一个着色器直接储存为多个字符串,这样具有更高的灵活度。"
|
||||||
|
Reference in New Issue
Block a user