写在前面
前面的几个例子使用的都是固定管线,又叫立即渲染模式,这个模式下 opengl 把渲染的细节封装隐藏起来,开发人员使用起来非常容易,但效率比较低且不够灵活。现代 opengl 使用的大多是可编程管线,又叫核心模式,从 opengl3.2 开始,固定管线就已经被废弃了,所以学习现代 opengl 就必须学可编程管线。虽然可编程管线上手比较难,但它能让我们操控 opengl 的底层,更有利于学习计算机图形学。
使用可编程管线需要用到“着色器”,也就是大名鼎鼎的 shader。着色器是运行在 GPU 中的,opengl 提供了一门着色器语言 GLSL,即 opengl shader language,还提供了相应的编译器,shader 程序必须编译后才能在 GPU 上运行。
shader
首先看看图形渲染管线的流程
GPU 接受一级顶点数据,经过一系列过程之后生成屏幕上显示的像素。管线包括下面几个过程
顶点着色器(Vertex Shader):它接收单一的顶点作为输入,然后把 3D 坐标转换成另一个 3D 坐标,主要做变换操作;顶点着色器允许我们对顶点属性做一些基本处理。
图元装配(Primitive Assembly):它接收顶点着色器输出的顶点作为输入,然后把顶点装配成指定的图元形状,如果是 GL_POINTS 是装配成点,如果是 GL_TRIANGLES,则装配成三角形。
几何着色器(Geometry Shader):它接收图元装配生成的图元的一系列顶点作为输入,通过产生新顶点构造出新的图元形状。
光栅化阶段(Rasterization Stage):它接收几何着色器生成的图元,将图元映射为屏幕上显示的像素,生成供下个阶段使用的片段(Fragment);在这个阶段会对片段进行裁切,把屏幕之外的片段丢弃掉。
片段着色器(Fragment Shader):它接收光栅化之后的片段数据,一个片段就是一个像素所需的所有数据;片段着色器计算一个片段(像素)的最终颜色,通常片段着色器包含 3D 场景的数据(如光照、阴影、光的颜色等),这些因素会影响像素最终的颜色。
Alpha 测试和混合(Blending)阶段:它检测片段的深度值,以确定该像素是在其它物体前面还是后面;还会检查 alpha 值并进行混合(blend),所以片段着色器处理过的颜色还不是最终的颜色,alpha 测试和混合也会影响颜色。
可以看到一个管线要经过上面六个过程,其最终目的是确定屏幕上绘制的像素点及其颜色,其中从顶点着色器到光栅化是根据顶点数据生成屏幕上的像素;片段着色器和混合是确定像素的最终颜色。这六个阶段使用到了三个着色器,这三个着色器是我们可以自定义的,其中顶点着色器和片段着色器是最重要的,也是我们必须自定义的,因为 GPU 中并没有默认的顶点着色器和片段着色器(这是针对可编程管线的,固定管线则不用我们自己定义任何着色器)。
前面介绍了固定管线是如何在屏幕上绘制图元的,现在知道了可编程管线的过程和 shader,我们再看看可编程管线如何一步步绘制图元。
输入顶点数据
无论是绘制一个点还是一个三角形,或是更复杂的模型,都需要先传入顶点数据(再复杂的模型也是由一个个顶点组成的)。顶点的一个最重要属性就是位置,位置在 opengl 中以 3D 坐标表示,所以一个顶点需要三个浮点数,表示它的位置。如果有多个顶点,可以以多维数组的方式来组织数据
GLfloat vertices[][3] = {
{ -0.5f, -0.5f, 0.0f },
{ 0.5f, -0.5f, 0.0f },
{ 0.0f, 0.5f, 0.0f }
};
也可以以一维数组的方式来组织数据
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
标准化坐标
顶点位置经过顶点着色器处理之后,其坐标应该是标准化设备坐标,否则该顶点不会进行绘制;标准化坐标指 x,y,z 值都在范围 -1.0~1.0 之间,超出这个范围的顶点将被丢弃(这一步在光栅化的时候处理)。
这些顶点数据将会传给顶点着色器,它将在 GPU 中创建内存(显存)来存储这些顶点数据,通常是经过顶点缓冲对象(VBO)来管理顶点。使用顶点缓冲对象的好处是可以一次性发送大批顶点数据到显卡,数据从 CPU 发送 GPU 速度是很慢的。
回顾 VBO
前面已经介绍过 VBO 的使用了,这里再复习一下。首先要创建顶点缓冲对象,创建的方法是给对象一个名字
GLuint VBO;
glGenBuffers(1, &VBO);
然后将对象绑定到 context,绑定的目标是 GL_ARRAY_BUFFER,绑定之后针对 GL_ARRARY_BUFFER 的操作都将作用于这个 VBO 上,直到我们绑定新的对象或者将这个 VBO 解绑。然后我们要把顶点数据复制到缓冲内存中,此时在 GL_ARRAY_BUFFER 上的缓冲调用都是配置当前绑定的 VBO