OpenGL系列-基本概念

欢迎来到 OpenGL 的世界。

OpenGL 到底是什么

一般它被认为是一个 API,包含一系列可以操作图形、图像的函数。然而,OpenGL 本身并不是一个 API,它仅仅是一个由Khronos组织制定并维护的规范。

OpenGL 规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现的,将由 OpenGL 库的开发者自行决定。

核心模式与立即渲染模式

早期的 OpenGL 使用立即渲染模式(Immediate mode,即固定渲染管线)在这个模式下绘制图形很方便。OpenGL 的大多数功能都被库隐藏了,开发者很少能控制 OpenGL 然后进行计算的自由。而随着时间推移,开发者迫切希望能有更多的灵活性,规范也越来越灵活,开发者对绘图细节有了更多的掌控。

立即渲染模式确实容易使用和立即,但是效率太低。因此从 OpenGL3.2 开始,规范文档开始废弃立即渲染模式,并鼓励开发者在 OpenGL 的核心模式下进行开发。

当使用核心模式时,OpenGL 迫使我们使用现代的函数,而现代函数要求使用者真正理解 OpenGL 和图形编程,它有一些难度,然而提过了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。

图形 API 的作用

图形 API 主要用于系统针对图片、视图、图层的渲染问题,游戏引擎针对人物、场景的渲染,视频播放框架中的视频解码渲染,以及核心动画、特效等操作。

除了 OpenGL 之外,还要DirectXOpenGL ESMetal

  • DirectX 是在 Windows 平台下的多媒体处理 API,不仅仅是图形 API
  • OpenGL ES 是 OpenGL 三维图形的子集,针对手机、PDA和游戏主机等嵌入式设备而设计,去除了许多不必要的、性能较低的 API 接口
  • Metal 是 Apple 为了解决 3D 渲染而推出的框架,能够为 3D 图形提高 10 倍的渲染性能

常见的专业名词解析

状态机

OpenGL 自身是一个巨大的状态机:一系列的变量描述 OpenGL 此刻应当如何运行。

OpenGL 的状态通常被称为 OpenGL 上下文

我们通常使用如下途径去更改 OpenGL 状态:

  • 设置选项
  • 操作缓冲

最后,我们使用当前 OpenGL 上下文来渲染。

假设当我们想告诉 OpenGL 去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变 OpenGL 状态,从而告诉 OpenGL 如何去绘图。

窗口/视口

OpenGL 渲染窗口的尺寸大小,即视口(Viewport),只有给定了视口,OpenGL 才能知道怎么根据窗口大小显示数据和坐标。可以通过调用glViewport来设置窗口的维度

glViewport(0, 0, 800, 600);

OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。

渲染

将图形/图像数据转换成 2D 空间图像操作叫做渲染(Rendering)

管线

在 OpenGL 中,任何事物都在 3D 空间中,而屏幕和窗口却是 2D 像素数组,这导致 OpenGL 的大部分工作都是关于把 3D 坐标转变为适应你屏幕的 2D 像素。3D 坐标转为 2D 坐标的处理过程是由 OpenGL 的图形渲染管线管理的。

图形渲染管线主要被划分为两个部分:

  1. 第一部分把你的 3D 坐标转换为 2D 坐标
  2. 第二部分是把 2D 坐标转变为时间的有颜色的像素

2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制

下图是图形渲染管线的每个阶段的抽象展示

pipeline

如你所见,图形渲染管线包含很多部分,每个部分都将在转换顶点数据到最终像素这一关过程中处理各自特定的阶段

顶点数组和顶点缓冲(VAO/VBO)

首先,以数组的形式传递 3 个 3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data)。

顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个 3D 坐标的数据的集合。而顶点数据是用顶点属性表示的,它可以包含任何我们想用的数据。

为了让 OpenGL 知道我们的坐标和颜色值构成的到底是什么,OpenGL 需要你去指定这些数据所表示的渲染类型。希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给 OpenGL。

其中的几个:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP

顶点着色器会在 GPU 上创建内存用于存储我们的顶点数据,还要配置 OpenGL 如何解释这些内存,并且指定其如何发送给显卡。

我们通过**顶点缓冲对象(Vertex Buffer Objects, VBO)**管理这个内存,它会在 GPU 内存中存储大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。

着色器与光栅化

图形渲染管线的第一个部分是**顶点着色器(Vertex Shader),**它把一个单独的顶点作为输入。

顶点着色器主要的目的是把 3D 坐标转换为另一个种 3D 坐标,同时顶点着色器允许我们对顶点属性进行一些基础处理。

**图元装配(Primitive Assembly)**阶段将顶点着色器输出的所有顶点作为输入,并所有的点装配成指定图元的形状。

图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。

几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器使用的片段。在片段着色器运行之前会指向裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

片段着色器(Fragment Shader) 的主要目的是计算一个像素的最终颜色,这也是所有 OpenGL 高级效果产生的地方。通常,片段着色器包含 3D 场景的数据(比如光照、阴影、光的颜色等),这些数据可以被用来计算最终像素的颜色。

OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据

混合

在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做 Alpha 测试混合阶段。

这个阶段检测片段的对应的深度值,用它们来判断这个像素是其他物体的前面还是后面,觉得是否应该丢掉。这个阶段也会检查alpha值并对物体进行混合(Blend)

纹理

我们可以为每个顶点添加颜色来增加图形的细节,从而创建出有趣的图形。但是,如果想让图形看起来更真实,我们就必须有足够多的顶点,从而指定足够多的颜色。这将会产生很多额外开销,因为每个模型都会需求更多的顶点,每个顶点又需求一个颜色属性。

因此,我们可以使用纹理(Texture)

纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理),它可以用来添加物体的细节。

纹理

你可以想象纹理是一张会有砖块的纸,无缝折叠贴合到你的 3D 的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。

除了图像以外,纹理也可以被用来储存大量的数据,这些数据可以发送到着色器上

变换

当我们想将创建的静态物体加入位移、缩放、旋转等变换时,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。

向量

向量最基本的定义就是一个方向。或者更正式的说,向量有一个方向(Direction)大小(Magnitude,也叫做强度或长度)

你可以把向量想像成一个藏宝图上的指示:“向左走10步,向北走3步,然后向右走5步”;“左”就是方向,“10步”就是向量的长度。那么这个藏宝图的指示一共有3个向量。向量可以在任意维度(Dimension)上,但是我们通常只使用2至4维。如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像),当它有3个维度的时候它可以表达一个3D世界的方向。

向量

标量

标量(Scalar)只是一个数字(或者说是仅有一个分量的向量)。当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算。

注意,数学上是没有向量与标量相加这个运算的,但是很多线性代数的库都对它有支持

矩阵

简单来说,矩阵就是一个矩形的数字、符合或表达式数组。矩阵中每一项叫做矩阵的元素。

矩阵

矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列2行,也叫做矩阵的维度(Dimension))。这与你在索引2D图像时的(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行)。

矩阵基本也就是这些了,它就是一个矩形的数学表达式阵列。和向量一样,矩阵也有非常漂亮的数学属性。矩阵有几个运算,分别是:矩阵加法、减法和乘法。

矩阵与向量相乘

很多有趣的 2D/3D 变换都可以放在一个矩阵中,用这个矩阵乘以我们的向量将变换这个这个向量。

例如,对一个向量进行缩放(Scaling)就是对向量的长度进行缩放,而保持它的方向不变。

我们可以顶一个有 2 个缩放遍历的向量 v = (3, 2),将向量沿着 x 轴缩放 0.5,使它的宽度缩小为原来的二分之一;沿 y 轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)倍所获得的的 s 是什么样的:

缩放

坐标系统

OpenGL 希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.01.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。

摄像机

OpenGL本身没有摄像机(Camera)的概念,但我们可以通过把场景中的所有物体往相反方向移动的方式来模拟出摄像机,产生一种我们在移动的感觉,而不是场景在移动。

总结

以上是 OpenGL 中常见的专业名词的解读,其中适当了加入了一些线性代数的扩展,以及图形渲染管线流程的分析。