OpenGL游戏引擎开发[3]-OpenGL的着色器GLSL

721 阅读7分钟

本节将学习OpenGL的着色器语言GLSL。

本节效果

www.bilibili.com/video/BV1jt…

主要内容

  1. 什么是着色器
  2. 如何在OpenGL中使用Shader
  3. 如何编写顶点着色器和片段着色器
  4. 代码实现过程

什么是着色器

前面说了,3D编程要完成2件事。

  1. 物体显示在哪?
  2. 物体显示成什么样?

着色器,也叫shader,字面意思就是用来着色的。当然,它也顺便完成了物体的空间变换。换句话说,3D渲染流水线就是在着色器中实现的,当然它要配合OpenGL的相关渲染指令来完成更加高级的操作,比如:帧缓冲区采样、像素缓冲区的应用等。

它是一小段类似于C语言的代码,强类型,需要编译成GPU可执行的指令。

每个3D渲染流水线的阶段对应不同的shader,完成不同的功能,我们常见的就是顶点着色器和片段着色器,这两个着色器可以满足我们日常绝大数需求。

如何在OpenGL中使用shader

OpenGL通过一个叫ShaderProrgam的对象来管理shader,所有的shader在使用前需要先附加到这个对象上,然后在使用的时候,使用这个对象来开启shader的使用。

那么shader什么时候使用呢?当然在绘制的时候,比如执行glDrawArrays()函数之前,我们要开启shader的使用,然后glDrawArrays()绘制的图形才会应用这个shader,在glDrawArrays()调用结束之后,就需要禁用当前的shader,以防止影响其他物体的绘制操作。这种编程模式可以总结为:

  1. 设置某个状态
  2. 执行某些操作
  3. 禁用前面设置的状态

这种编程模式在OpenGL编程过程中会经常用到,因为OpenGL本身是基于状态机的,这也是处于性能的考虑。OpenGL每次执行特定的指令时,如果该指令依赖特定的状态,它就会去检查该状态是否被设置过,如果设置过则直接使用之前的状态,如果你不更改,则接下来所有的这些指令都会使用这个状态,否则使用默认状态。所以,我们要养成:设置状态->执行操作->禁用状态 这个编程习惯,这样你当前设置的状态只会对你接下来的操作有影响,而不会影响程序的其他部分。

在OpenGL中使用shader的步骤,也不是太复杂:

  1. 写shader源码
  2. 创建ShaderProgram对象,编译shader源码,附加shader源码到ShaderProgram对象上
  3. 使用shader

其中,第二、三步比较固定,请查看源码中的ShaderProgam.h/cpp的实现,主要就是shader如何写?

如何写Shader

写之前,我们要了解shader的大致结构,其实很简单,下面是一个基本的着色器的结构。

顶点着色器:

片段着色器,结构和顶点着色器是一致的。

  1. 版本声明部分

声明shader的版本,因为不同版本的shader,其中的关键字、内置变量、内置函数是有差异的。而且,OpenGL es 使用的shader也pc版OpenGL使用的shader也是略有差异的。

  1. 输入顶点属性部分

顶点属性,顾名思义就是顶点的属性。因为顶点不管具有空间位置,还有其他的属性,比如:法线方向,颜色,纹理坐标等。比如,一个游戏人物角色,顶点的空间位置只是描述了人物的外形,而人物一般都是贴了纹理的,所以每个顶点还要存储它对应的纹理坐标,通过这个坐标OpenGL才能正确去纹理图片中采样颜色,进而显示贴了图的人物角色。上面的着色器有2个顶点输入属性,顶点的位置和顶点的颜色,这里我们没有使用纹理, 我们留着在下一节讲。

  1. uniform变量

有的书上翻译为一致变量,读着好难受啊。顾名思义,uniform就是统一、相同、一致的意思,就是说,这个变量要作用到每个顶点上。这里需要说明的是,每个顶点都会走一遍3D渲染管线,也就是说每个顶点都会经过顶点着色器,片段着色器,几何着色器,计算着色器等,当然顶点着色器和片段着色器是必须的。uniform变量就是对每个顶点起相同作用的变量,比如:每个顶点都需要经过世界变换、相机变换、投影变换才能显示到屏幕上,那么这些变换矩阵可以声明为uniform变量。如果你有其他需求,当然可以增加自己的uniform变量。

  1. out变量

表明这个变量是输出到下一个阶段着色器中的。图中在顶点着色器中输出顶点的颜色到下一个着色器也就是片段着色器。有人问,你啥也没干,就直接输出给下一个片段着色器了?那什么不在片段着色器中直接写个in color变量?

因为流水线之所以是流水线,是因为它是有序的。我们开始3D渲染经过的第一个阶段是顶点着色器,然后是片段着色器…我们所有in变量必须在顶点着色器中设置,然后传递到其他的着色器中。当然,除非你声明为uniform变量,该变量可在3D渲染管线的任意阶段进行设置。

  1. main函数

每个着色器都会有1个main函数, 语法类似于C语言的语法。

上面的顶点着色器和片段着色器的功能是,给定顶点的位置和颜色,根据给定的变换矩阵(前面教程介绍过)变换到标准化设备坐标系中,赋值给 gl_Position,这是glsl的内置变量。

接着将顶点的颜色传递到片段着色器,片段着色器啥也没干,直接将顶点的颜色输出到下一阶段进行处理。

代码实现过程

  1. 首先,我们新建一个ShaderProgram类,封装处理shader的过程:编译、链接、开启、禁用shader等。
  1. 然后根据我们自己的实际需求来自定义自己的shader,比如,你绘制地形使用的shader和绘制太阳的shader肯定是不同的。太阳的shader要“发光、耀眼”,而你地形的shader主要是贴纹理,实现雾效果等。每种显示效果的实现,都需要定义自己的shader。我们在代码实现中,给了2个shader,一个用来渲染带颜色的三角形basicShader,还有1个是渲染一个线框地球EarthShader。等我们讲纹理的时候,我们来渲染一个带贴图的地球。现在,这个两个shader很简单,所以基本框架代码都类似,后面我们不断丰富EarthShader。
  1. 然后,我们定义了一个EarthRenderer专门来负责渲染地球。它在MasterRenderer中被调用,这样我们就可以将渲染特定物体的代码从MasterRenderer中分离,方便维护。
  1. 接着就可以开始渲染了。

首先我们需要准备渲染的数据,这里我们是代码生成的,当然我们可以外部加载3D建模软件中建好的模型,这个后面我们也会介绍如何自己编码实现加载外部模型,以及如何使用开源的库来加载外部模型。

然后我们就可以在帧循环中渲染了。为了演示,现在的函数接口写得很“糙”,后面会随着需求进行优化的。

  1. 此时,你应该能看到本文一开始的效果了。

本节源码:https://ww.lanzous.com/icfc4ud

ok,本节结束。

下一节,我们绘制一个带纹理的地球。