type
status
date
slug
summary
tags
category
icon
password
渲染管线
物体从三维空间中渲染到屏幕上是通过渲染管线完成的,它接受一组3D坐标并转变成屏幕上的2D像素输出。OpenGL的图形渲染管线被划分为几个阶段,每个阶段将前一个阶段的输出作为输入。每一个阶段都有自己的程序在GPU上运行,这些程序就是着色器。
OpenGL的着色器使用的OpenGL着色器语言写成,下面是渲染管线各个阶段的抽象表示,蓝色部分是我们可以自定义着色器的部分。

图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。
顶点着色器阶段的输出可以选择性地传递给几何着色器(Geometry Shader)。几何着色器将一组顶点作为输入,这些顶点形成图元,并且能够通过发出新的顶点来形成新的(或其他)图元来生成其他形状。在这个例子中,它从给定的形状中生成第二个三角形。
图元装配(Primitive Assembly)阶段将顶点着色器(或几何着色器)输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定图元的形状;图中例子中是两个三角形。
图元装配阶段的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
片段着色器的主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
在现代OpenGL,必须定义一个顶点着色器和一个片段着色器。
顶点输入
本篇需要绘制一个2D三角形,首先需要输入一个三角形三个点的坐标,定义一个float数组
因为是2D的,因此z轴的值都是0。OpenGL只会处理-1.0f到1.0f的坐标,这个范围内的坐标为标准化设备坐标。
OpenGL通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在显存存储大量顶点。这里通过glGenBuffer生成带有缓冲ID的VBO对象。第一个参数是指要生成的缓冲区v的数量,第二个是存储缓冲对象名称的数组。
通过glBindBuffer将新创建的缓冲绑定到顶点缓冲对象上,GL_ARRAY_BUFFER是顶点缓冲对象的缓冲类型。
通过glBufferData将上面创建的顶点数据vertices复制到缓冲的内存中。第一个参数是是复制到哪个缓冲类型,第二个参数是指定传输数据的大小,第三个参数是发送的实际数据,第四个参数指定希望显卡怎么管理给定的数据。有GL_STATIC_DRAW(数据几乎不改变)、GL_DYNAMIC_DRAW(数据会被改变很多)、GL_STREAM_DRAW(数据每次绘制都会改变)。因为这里只会绘制单一的三角形,数据不会改变因此使用GL_STATIC_DRAW即可,使用GL_DYNAMIC_DRAW和GL_STREAM_DRAW会将数据放在显卡放在可以告诉写入的内存部分。
着色器
接下来创建着色器来处理这些数据。创建顶点着色器文件VertexShader.glsl并编写以下代码
然后需要读取这些代码
接下来创建顶点着色器,传入的参数是GL_VERTEX_SHADER,如果创建片段着色器传入的参数就是GL_FRAGMENT_SHADER
将读取的代码通过glShaderSouce附着到着色器对象上并通过glCompileShader编译。glShaderSource的第一个参数是要编译的着色器对象,第二个参数指定源码字符串的数量,第三个参数是传入的源码,第四个参数是一个数组,包含第三个参数每个字符串的长度,如果是空的话就认为每个字符串都以空字符串结束。
可以通过glGetShaderiv和glGetShaderInfoLog来判断是否编译成功
创建片段着色器同上步骤,创建片段着色器文件
接下来需要链接编译好的着色器,首先通过glCreateProgram创建一个着色器程序对象
再将上面创建的shader链接到program上
同样地,可以获取链接的信息
最后指定使用的这个程序并删除编译好的不需要的shader
链接顶点属性
回到上面输入的顶点,在渲染之前需要指定OpenGL怎么解析输入的顶点数据。下面是这些数据希望被解析的格式。

通过glVertexAttribPointer来告诉OpenGL怎么解析顶点数据。
第一个参数是要配置的顶点属性,顶点着色器中使用layout(location = 0)定义position的顶点属性的位置,它被定义的位置值是0,因为这里希望将数据传入到这个顶点属性中,所以这里传入的也是0。
第二个参数是顶点属性的大小,顶点属性是vec3,由三个值组成因此是3。
第三个参数是指定该数据的类型,这里是GL_FLOAT,glsl中的vec都是由浮点值组成的。
第四个参数定义我们是否希望数据被标准化(Normalize)。
第五个参数是步长,告诉OpenGL连续的顶点属性组之间的间隔。这里一个顶点由三个float表示,因此是3*sizeof(float)。
第六个参数告诉OpenGL数据在缓冲中起始位置的偏移量(Offset),这里因为位置数据在数组开头,所以是0,以为参数是void*类型,这里要进行一个参数转换。
glEnableVertexAttribArray传入指定要启用的顶点属性的索引,因为这里的顶点属性是0,因此传入0。
每次绘制都要向上面这样生成VBO,绑定数据,设置顶点属性,非常麻烦,性能也很低下,因此引入了顶点数组对象(Vertex Array Object, VAO)去解决这一问题。将顶点属性的调用存储在VAO中,然后解绑VAO。后面需要用到这样的顶点属性的时候再绑定再绘制即可。

首先创建一个VAO
绑定了之后再设置顶点属性
最后绘制它!
元素缓冲对象
新的问题出现了,如果要绘制一个矩形,就要使用两个三角形,六个顶点。但是绘制一个矩阵只需要四个顶点,怎么通过顶点的复用解决这个问题呢?元素缓冲对象(Element Buffer Object, EBO)是一个很好的解决方法。它通过对顶点的索引来达到复用。
首先定义顶点和顶点的索引
老方法,生成一个EBO
然后绑定并复制数据
最后绘制
glDrawElements第一个参数是指定绘制的模式,第二个参数是绘制的顶点的个数,第三个参数是索引的类型,第四个参数是索引数组的偏移量。
绑定VAO的时候会在glBindBuffer存储EBO的数据,因此不用每次绘制都要绑定EBO,只要绑定了VAO即可。

GLSL
这里的着色器是用GLSL编写的,具有以下的经典结构
变量
着色器的输入变量又叫顶点属性(Vertex Attribute),能声明的顶点属性是有上限的,一般由硬件决定,OpenGL确保由16个包含4分量的顶点属性使用,可以根据GL_MAX_VERTEX_ATTRIBS查询具体的上限。
GLSL由以下的变量类型
vecn | n个float分量的默认向量 |
bvecn | n个bool分量的向量 |
ivecn | n个int分量的向量 |
uvecn | n个unsigned int分量的向量 |
dvecn | n个double分量的向量 |
通过.x, .y, .z, .w来获取它们的滴1,2,3,4个分量。
输入输出
GLSL通过in和out来专门实现输入和输出。每个着色器的输入应该与上一个着色器的输入相匹配,而顶点着色器的输入比较特殊,它从顶点数据中直接接收输入,可以通过location这一元数据指定输入变量,这样就可以在CPU上配置顶点属性。
片段着色器的输入和输出就是用in和out就好了
示例:
VertexShader
FragmentShader
Uniform
uniform是一种从应用程序将数据从CPU传递到GPU的一种方式,但是它和顶点属性不同,它是全局的,可以在任意着色器在任意阶段被访问。可以通过uniform来定义三角形的颜色
如果声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
然后通过找到着色器中uniform属性的位置值并更新
- Author:lltouchingfish
- URL:https://tangly1024.com/article/202be847-332a-80fc-b163-c9f2a72df6b7
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!