OpenGL笔记

文档阅读笔记

Posted by 袁平 on October 13, 2018

前言

阅读OpenGL中文文档笔记

中文文档


正文


一. 入门

1.1 OpenGL

  1. 什么是OpenGL: 并不是API, 而是一个规范; OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定; OpenGL基于显卡, 所以OpenGL API大多由显卡厂商提供

  2. 核心模式与立即渲染模式

  1. 立即渲染模式(固定渲染管线): 用于早期OpenGL; 封装性好, 使用方便, 但是效率低, 灵活性不够
  2. 核心模式: 从OpenGL3.2开始, 规范文档开始废弃立即渲染模式; 当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图
  1. 状态机: OpenGL状态通常被成为OpenGL上下文; OpenGL本质上是一个大状态机(理解状态函数和状态使用函数)

1.2 Hello Window

  1. 视口: ViewPort; OpenGL渲染窗口的尺寸大小

  2. 双缓冲: 应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了

1.3 Hello 三角形

  1. 图形渲染管线: 又称为管线; 指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程; 在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素; 3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线管理的; 图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素

  2. 着色器: 图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器; 而且因为它们运行在GPU上,所以它们可以给我们节约宝贵的CPU时间

  3. 如下为每个图形渲染管线的抽象展示; 其中蓝色部分是可以注入自定义着色器的部分

图形渲染管线

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

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

  3. 顶点着色器(Vertex Shader): 顶点着色器主要的目的是把3D坐标转为另一种3D坐标, 同时顶点着色器允许我们对顶点属性进行一些基本处理

  4. 图元装配(Primitive Assembly): 将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状; 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状

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

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

  7. 在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

  8. 标准化设备坐标: 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)

标准化设备坐标

  1. 深度: 通常深度可以理解为z坐标,它代表一个像素在空间中和你的距离,如果离你远就可能被别的像素遮挡,你就看不到它了,它会被丢弃,以节省资源。

  2. 着色器程序: 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用

  3. glVertexAttribPointer

glVertexAttribPointer

1.4 GLSL

  1. 着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信

  2. GLSL基本数据类型: int, float, double, uint, bool

  3. 容器类型: Vector, Matrix

  4. 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)

  5. Uniform: Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式, 但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

  6. 如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

1.5 纹理

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

  2. 为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。

  3. 采样: 使用纹理坐标获取纹理颜色叫做采样

  4. 纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角; 如下图

纹理坐标

  1. 纹理环绕方式: 纹理坐标的范围通常是从(0, 0)到(1, 1),那如果我们把纹理坐标设置在范围之外会发生什么?OpenGL默认的行为是重复这个纹理图像

纹理环绕方式

  1. 纹理过滤: 纹理坐标不依赖于分辨率(Resolution),它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel,译注1)映射到纹理坐标(Texture Pixel也叫Texel,你可以想象你打开一张.jpg格式图片,不断放大你会发现它是由无数像素点组成的,这个点就是纹理像素;注意不要和纹理坐标搞混,纹理坐标是你给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色); 纹理过滤主要有两种: GL_NEAREST和GL_LINEAR; 当进行放大(Magnify)和缩小(Minify)操作的时候可以设置纹理过滤的选项(使用glTexParameteri())

  2. 邻近过滤(GL_NEAREST): 默认纹理过滤方式; 当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素

  3. 线性过滤(GL_LINEAR): 会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大(GL_LINEAR可以产生更真实的输出)

  4. 多级渐远纹理: 简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一; 多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好(glGenerateMipmaps())

  5. 纹理单元: 一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的激活纹理单元; 纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先激活对应的纹理单元

1.5 变换

  1. 向量: 有大小和方向; 其实就是一个N*1的矩阵

  2. 向量运算:

  1. 向量与标量运算

向量与标量运算

  1. 向量取反: 个分量都取反
  2. 向量之间加减: 各分量之间对应加减
  3. 向量乘积

点乘: 点乘是通过将对应分量逐个相乘,然后再把所得积相加来计算的

点乘

点乘2

叉乘:

叉乘

  1. 矩阵运算
  1. 矩阵与标量运算

矩阵与标量运算

  1. 矩阵之间加减: 对应项加减即可
  2. 矩阵数乘: 将该数和矩阵每一项想乘
  3. 矩阵相乘: 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘; 矩阵相乘不遵守交换律
  4. 矩阵与向量相乘:

缩放:

矩阵与向量运算

位移:

矩阵与向量运算

沿x轴旋转:

沿x轴旋转

沿y轴旋转:

沿y轴旋转

沿z轴旋转:

沿z轴旋转

1.6 坐标系统

  1. 将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易

  2. 对我们来说比较重要的有五类坐标系统

  1. 局部空间(Local Space,或者称为物体空间(Object Space))
  2. 世界空间(World Space)
  3. 观察空间(View Space,或者称为视觉空间(Eye Space))
  4. 裁剪空间(Clip Space)
  5. 屏幕空间(Screen Space)
  1. 为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵

  2. 顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束; 如下图

坐标系统

  1. 之所以将顶点变换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等

  2. 局部空间: 物体所在的坐标空间,即对象最开始所在的地方

  3. 世界空间: 物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的

  4. 观察空间: 观察空间经常被人们称之OpenGL的摄像机(Camera); 观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果; 因此观察空间就是从摄像机的视角所观察到的空间; 这通常用一个观察矩阵(View Matrix)完成,它被用来将世界坐标变换到观察空间

  5. 裁剪空间: 在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来; 为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标(如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围)(一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行)

  6. 透视投影: 离你越远的东西看起来更小; 使用透视投影矩阵来完成的; 一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上, 即将顶点坐标中的每个分量除以w分量; 透视投影如下图; 位于视景体之内的物体被投影到金字塔的顶点, 也就是照相机或观察点的位置; 靠近观察点的物体看上去更大一些, 因为和远处的物体相比, 它占据了视景体中相对较大的区域(实际上从相机(观察点)的角度看顶面和底面, 其占据的空间大小相同, 都是视口大小); 平截头体的四个侧面, 顶面和底面对应与视景体的6个裁剪平面, 位于这些平面之外的物体将被裁剪掉, 不会出现在最终的图像中;

透视投影

  1. 一个顶点坐标会根据以下过程被变换到裁剪坐标(注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)): 最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪

裁剪坐标

  1. OpenGL存储深度信息在一个叫做Z缓冲(Z-buffer)的缓冲中,它允许OpenGL决定何时覆盖一个像素而何时不覆盖

  2. Z缓冲(深度缓冲): 深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的

  3. 视景体 : 管线会确定的一个可视空间区域, 由 上平面(up), 下平面(down), 左平面(left), 右平面(right), 远平面(far), 近平面(near) 六个平面组成

  4. 视景体与投影 : 视景体内的物体会投影到近平面, 视景体之外的内容会被裁减掉, 例如眼睛看不到的范围就是处于视景体外即被裁减掉的

1.7 摄像机

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

二. 光照

2.1 颜色

  1. 物体的颜色为物体从一个光源反射各个颜色分量的大小

  2. 物体的颜色是其和光源颜色的向量乘积

颜色

2.2 基础光照

  1. 冯氏光照模型(Phong Lighting Model): 冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照

冯氏光照模型

  1. 当计算光照时我们通常不关心一个向量的模长或它的位置,我们只关心它们的方向。所以,几乎所有的计算都使用单位向量完成,因为这简化了大部分的计算(比如点乘)。所以当进行光照计算时,确保你总是对相关向量进行标准化,来保证它们是真正地单位向量。忘记对向量进行标准化是一个十分常见的错误

  2. 每当我们应用一个不等比缩放时(注意:等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复),法向量就不会再垂直于对应的表面了,这样光照就会被破坏; 如下图; 修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix)

缩放导致法向量破坏

2.3 材质

  1. 在现实世界里,每个物体会对光产生不同的反应; 有些物体反射光的时候不会有太多的散射(Scatter),因而产生一个较小的高光点,而有些物体则会散射很多,产生一个有着更大半径的高光点。如果我们想要在OpenGL中模拟多种类型的物体,我们必须为每个物体分别定义一个材质(Material)属性。

  2. 可以用这三个分量来定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting), 再添加一个反光度(Shininess), 就能够对物体的颜色输出有着精细的控制了

  3. 我们为每个冯氏光照模型的分量都定义一个颜色向量。ambient材质向量定义了在环境光照下这个物体反射得是什么颜色,通常这是和物体颜色相同的颜色。diffuse材质向量定义了在漫反射光照下物体的颜色。(和环境光照一样)漫反射颜色也要设置为我们需要的物体颜色。specular材质向量设置的是镜面光照对物体的颜色影响(或者甚至可能反射一个物体特定的镜面高光颜色)。最后,shininess影响镜面高光的散射/半径

具体材质的该四个分量可以参见: http://devernay.free.fr/cours/opengl/materials.html

  1. 物体过亮的原因是环境光、漫反射和镜面光这三个颜色对任何一个光源都会去全力反射。光源对环境光、漫反射和镜面光分量也具有着不同的强度

2.4 光照贴图

  1. 漫反射贴图

  2. 镜面光贴图

2.5 投光物

  1. 平行光: 当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的

  2. 点光源: 定向光对于照亮整个场景的全局光源是非常棒的,但除了定向光之外我们也需要一些分散在场景中的点光源(Point Light)。点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减

  3. 聚光: 聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。这样的结果就是只有在聚光方向的特定半径内的物体才会被照亮,其它的物体都会保持黑暗。聚光很好的例子就是路灯或手电筒。