【OpenGL】入门篇

https://learnopengl-cn.github.io/
http://www.opengl-tutorial.org/


目录

OpenGL概述

扩展

状态机

对象

Pipeline

Vertex Data(CPU→显存/GPU内存)

VBO(缓冲类型GL_ARRAY_BUFFER)

VAO(配置顶点属性指针)

EBO(缓冲类型GL_ELEMENT_ARRAY_BUFFER)

Veterx Shader

M(Object Space→World Space)

V(World Space→View Space)

P(View Space→Projection Space)

Shape Assembly

Tessellation Shader

Geometry Shader

裁剪

屏幕映射

Rasterization

Fragment Shader

着色器的链接

uniform数据

Strencil Testing

Depth Testing

Early Depth Testing

深度值精度

转存失败重新上传取消​Z-fighting

Alpha Testing

Blending

模型

Assimp(Open Asset Import Library)

纹理

Wrapping

Filtering

Nearest Neighbor Filtering

Linear Filtering

Anisotropic filtering

Mipmap

纹理单元

压缩纹理

灯光

平行光

点光源

衰减(Attenuation)

聚光灯

软化边缘

基本着色

法线与法线矩阵

材质颜色

环境光照(Ambient Lighting)

漫反射光照(Diffuse Lighting)

镜面光照(Specular Lighting)

半程向量(Halfway Vector)


OpenGL概述

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

OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者自行决定。由于OpenGL的大多数实现都是由显卡厂商编写的,当产生一个bug时通常可以通过升级显卡驱动来解决。这些驱动会包括你的显卡能支持的最新版本的OpenGL,这也是为什么总是建议你偶尔更新一下显卡驱动。

扩展

OpenGL的一大特性就是对扩展(Extension)的支持,当一个显卡公司提出一个新特性或者渲染上的大优化,通常会以扩展的方式在驱动中实现。通常,当一个扩展非常流行或者非常有用的时候,它将最终成为未来的OpenGL规范的一部分。

状态机

OpenGL自身是一个巨大的状态机(State Machine)一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)

  • 状态设置函数(State-changing Function),这类函数将会改变上下文。
  • 状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作

对象

在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。

  • 我们首先创建一个对象,然后用一个id保存它的引用(实际数据被储存在后台)。
  • 然后我们将对象绑定至上下文的目标位置。
  • 接下来我们设置选项
  • 最后我们将目标位置的对象id设回0,解绑这个对象。

设置的选项将被保存在objectId所引用的对象中,一旦我们重新绑定这个对象到目标位置,这些选项就会重新生效。


Pipeline

【OpenGL】入门篇

Vertex Data(CPU→显存/GPU内存)

定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡

VBO(缓冲类型GL_ARRAY_BUFFER

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

glVertexAttribPointer(如何解析数据)每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。

VAO(配置顶点属性指针)

绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。

一个顶点数组对象会储存以下这些内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。

【OpenGL】入门篇

EBO(缓冲类型GL_ELEMENT_ARRAY_BUFFER

【OpenGL】入门篇

Veterx Shader

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

M(Object Space→World Space)

V(World Space→View Space)

The engines don’t move the ship at all. The ship stays where it is and the engines move the universe around it.

P(View Space→Projection Space)

仅有x、y坐标还不足以确定物体是否应该画在屏幕上:它到摄像机的距离(z)也很重要!两个x、y坐标相同的顶点,z值较大的一个将会最终显示在屏幕上,这就是所谓的透视投影(perspective projection)。
向量与投影矩阵相乘之后,齐次坐标的每个分量都要除以自身的W(透视除法)。W分量恰好是-Z(投影矩阵会保证这一点)。这样,离原点更远的点,除以了较大的Z值;其X、Y坐标变小,点与点之间变紧密,物体看起来就小了,这才产生了透视效果。

【OpenGL】入门篇【OpenGL】入门篇

【OpenGL】入门篇【OpenGL】入门篇

Shape Assembly

将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状(如:GL_TRIANGLES、GL_LINE_STRIP)

Tessellation Shader

Geometry Shader

几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。

几何着色器的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。

因为这些形状是在GPU的超快硬件中动态生成的,这会比在顶点缓冲中手动定义图形要高效很多。因此,几何缓冲对简单而且经常重复的形状来说是一个很好的优化工具,比如体素(Voxel)世界中的方块和室外草地的每一根草。

裁剪

去除NDC外的。

屏幕映射

Rasterization

把图元映射为最终屏幕上相应的像素生成供片段着色器(Fragment Shader)使用的片段(Fragment,个片段是OpenGL渲染一个像素所需的所有数据)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。

Fragment Shader

主要目的是计算一个像素的最终颜色

着色器的链接

着色器程序对象(Shader Program Object)多个着色器对象合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候**这个着色器程序。已**着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

uniform数据

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。

Strencil Testing

  • 启用模板缓冲的写入。
  • 渲染物体,更新模板缓冲的内容。
  • 禁用模板缓冲的写入。
  • 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。

Depth Testing

OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。

深度缓冲是由窗口系统自动创建的,它会以16、24或32位float的形式储存它的深度值。在大部分的系统中,深度缓冲的精度都是24位的。

Early Depth Testing

现在大部分的GPU都提供一个叫做提前深度测试(Early Depth Testing)的硬件特性。提前深度测试允许深度测试在片段着色器之前运行。只要我们清楚一个片段永远不会是可见的(它在其他物体之后),我们就能提前丢弃这个片段。片段着色器通常开销都是很大的,所以我们应该尽可能避免运行它们。不能和Alpha Test一起用,若前面进行了像素丢弃,但后方无像素绘制。

深度值精度

在实践中是几乎永远不会使用线性深度缓冲(Linear Depth Buffer)的。

要想有正确的投影性质,需要使用一个非线性的深度方程,它是与 1/z 成正比的。它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。

【OpenGL】入门篇

【OpenGL】入门篇

Z-fighting

一个很常见的视觉错误会在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。这个现象叫做深度冲突

解决方案:

  • 永远不要把多个物体摆得太靠近以至于它们的一些三角形会重叠
  • 尽可能将*面设置远一些在前面我们提到了精度在靠近*面时是非常高的,所以如果我们将*面远离观察者,我们将会对整个平截头体有着更大的精度。
  • 牺牲一些性能,使用更高精度的深度缓冲。大部分深度缓冲的精度都是24位的,但现在大部分的显卡都支持32位的深度缓冲,这将会极大地提高精度。

Alpha Testing

检测一个片段的alpha值是否低于某个阈值,如果是的话,则丢弃这个片段

Blending

【OpenGL】入门篇

顺序很重要:

  • 渲染场景的不透明部分,让深度缓冲丢弃被遮挡的透明三角形。
  • 对透明三角形按深度由远及近排序深度测试和混合一起使用的话会产生一些麻烦
  • 渲染透明三角形。

存在的问题:

  • 您将受限于填充率。亦即每个片段会写10、20次,也许更多次。这对力不从心的内存总线来说太繁重了。通常,深度缓冲可以自动丢弃”远”片段;但这里我们已显式地对片段进行了排序,因此深度缓冲实际上没起作用。
  • 透明三角形排序很耗时
  • 若要逐个三角形地切换纹理,或者更糟糕,切换着色器–性能会大打折扣。不要这么做。

解决方案:

  • 限制透明多边形的数量
  • 对所有透明多边形使用同一个着色器和纹理
  • 若这些透明多边形本就是外表各异的,那就用不同的纹理
  • 若不排序时效果还凑合,那就不排序好了。

模型

Assimp(Open Asset Import Library)


纹理

纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;除了图像以外,纹理也可以被用来储存大量的数据,这些数据可以发送到着色器上。

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

【OpenGL】入门篇

 

Wrapping

【OpenGL】入门篇

Filtering

Nearest Neighbor Filtering

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

【OpenGL】入门篇

Linear Filtering

基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大

【OpenGL】入门篇

Anisotropic filtering

这种方法逼近了真正片断中的纹素区块。例如下图中稍稍旋转了的纹理,各向异性过滤将沿蓝色矩形框的主方向,作一定数量的采样(即所谓的”各向异性层级”),计算出其内的颜色。

但若想获得更高质量的纹理,可以采用各向异性过滤,不过速度有些慢。

【OpenGL】入门篇

Mipmap

有些物体会很远,但其纹理会拥有与近处物体同样高的分辨率由于远处的物体可能只产生很少的片段,OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物体上这会产生不真实的感觉,更不用说对它们使用高分辨率纹理浪费内存的问题了。

多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好【OpenGL】入门篇

  • 一开始,把图像缩小到原来的1/2,然后依次缩小,直到图像只有1x1大小(应该是图像所有纹素的平均值)
  • 绘制模型时,根据纹素大小选择合适的mipmap。
  • 可以选用nearest、linear、anisotropic等任意一种滤波方式来对mipmap采样。
  • 要想效果更好,可以对两个mipmap采样然后混合,得出结果。
  • 在渲染中切换多级渐远纹理级别(Level)时,会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。

纹理单元

你可能会奇怪为什么sampler2D变量是个uniform,我们却不用glUniform给它赋值。使用glUniform1i,我们可以给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)。一个纹理的默认纹理单元是0,它是默认的**纹理单元。纹理单元的主要目的是让我们在着色器中可以使用多于一个的纹理。通过把纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们首先**对应的纹理单元。

压缩纹理

  • 下载The Compressonator,一款ATI工具
  • 用它加载一个二次幂纹理
  • 将其压缩成DXT1、DXT3或DXT5格式(这些格式之间的差别请参考Wikipedia)
  • 生成mipmap,这样就不用在运行时生成mipmap了。
  • 导出为.DDS文件。

至此,图像已压缩为可被GPU直接使用的格式。在着色中随时调用texture()均可以实时解压。这一过程看似很慢,但由于它节省了很多内存空间,传输的数据量就少了。传输内存数据开销很大;纹理解压缩却几乎不耗时(有专门的硬件负责此事)一般情况下,采用压缩纹理可使性能提升20%。

DXT压缩源自DirectX。和OpenGL相比,DirectX中的V纹理坐标是反过来的。所以使用压缩纹理时,得用(coord.v, 1.0-coord.v)来获取正确的纹素。可以在导出脚本、加载器、着色器等环节中执行这步操作。

灯光

平行光

当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。不论物体和/或者观察者的位置,看起来好像所有的光都来自于同一个方向。定向光非常好的一个例子就是太阳。

【OpenGL】入门篇

点光源

它会朝着所有方向发光,但光线会随着距离逐渐衰减。想象作为投光物的灯泡和火把,它们都是点光源。

【OpenGL】入门篇

衰减(Attenuation)

【OpenGL】入门篇

常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。

随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。随距离减少光强度的一种方式是使用一个线性方程。这样的方程能够随着距离的增长线性地减少光的强度,从而让远处的物体更暗。然而,这样的线性方程通常会看起来比较假。在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。

聚光灯

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

【OpenGL】入门篇

软化边缘

为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。

【OpenGL】入门篇

这里ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)。最终的I值就是在当前片段聚光的强度。

基本着色

法线与法线矩阵

法向量是一个垂直于顶点表面的(单位)向量。由于顶点本身并没有表面(它只是空间中一个独立的点),我们利用它周围的顶点来计算出这个顶点的表面。顶点的法线,是包含该顶点的所有三角形的法线的均值。

如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。等比缩放不会破坏法线,因为法线的方向没被改变,仅仅改变了法线的长度,而这很容易通过标准化来修复

【OpenGL】入门篇

Normal = mat3(transpose(inverse(model))) * aNormal;

材质颜色

我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。

【OpenGL】入门篇

环境光照(Ambient Lighting)

光的一个属性是,它可以向很多方向发散并反弹,从而能够到达不是非常直接临近的点。所以,光能够在其它的表面上反射,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法,但是这种算法既开销高昂又极其复杂。

漫反射光照(Diffuse Lighting)

模拟光源对物体的方向性影响(Directional Impact)。物体的某一部分越是正对着光源,它就会越亮(更少的点会被照射到,总光强度仍然是一样的)。

【OpenGL】入门篇

镜面光照(Specular Lighting)

模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。

【OpenGL】入门篇

半程向量(Halfway Vector)

【OpenGL】入门篇

右图视线与反射方向之间的夹角明显大于90度,这种情况下镜面光分量会变为0.0。这在大多数情况下都不是什么问题,因为观察方向离反射方向都非常远。然而,当物体的反光度非常小时,它产生的镜面高光半径足以让这些相反方向的光线对亮度产生足够大的影响。

1977年,James F. Blinn在冯氏着色模型上加以拓展,引入了Blinn-Phong着色模型。Blinn-Phong模型不再依赖于反射向量,而是采用了所谓的半程向量(Halfway Vector),即光线与视线夹角一半方向上的一个单位向量。当半程向量与法线向量越接近时,镜面光分量就越大。

【OpenGL】入门篇

现在,不论观察者向哪个方向看,半程向量与表面法线之间的夹角都不会超过90度(除非光源在表面以下)。它产生的效果会与冯氏光照有些许不同,但是大部分情况下看起来会更自然一点,特别是低高光的区域。

除此之外,冯氏模型与Blinn-Phong模型也有一些细微的差别:半程向量与表面法线的夹角通常会小于观察与反射向量的夹角。所以,如果你想获得和冯氏着色类似的效果,就必须在使用Blinn-Phong模型时将镜面反光度设置更高一点。通常我们会选择冯氏着色时反光度分量的2到4倍。