LearnOpenGL
OpenGL
图形渲染接口 (Linux,Windows,MacOS,iOS,Android跨平台图形接口,底层库实现由显卡厂商(AMD NVIDIA)或系统厂商(APPLE)实现)
渲染模式: Immediate mode OR Core-profile
Immediate mode(已废弃):
流水线固定渲染模式,这个模式下,所有步骤固定,只能控制整个过程中的一些阶段中一些功能的开启或者关闭
Core-profile:
核心模式,这个模式下,所有流程中的几个阶段不仅可以开启或者关闭,还可以自定义设置数据,比Immediate mode的编程自由性更强
OpenGL 整个渲染过程是一个流水的加工处理改变状态的一个过程,整体为一个状态机。
一瓶纯净水流水生产流程,从入口经过过滤、消毒、蒸馏、罐装、封瓶、封箱等一系步骤下来的一个过程。
图形渲染也类似,经过定义数据、生成图形、渲染颜色、输出至屏幕这么个大概过程,当然整个过程很复杂,前面几个简述步骤下还要细分,并且涉及到很多知识点尤其数学知识(线性代数,三角学)
OpenGL仅是一个图形渲染接口,很多其他功能并不提供,我们需要一些其他库来帮我们来完成环境开发的搭建
1.GLFW 窗口,输入处理
是一款开源、跨平台的图形窗口管理库,不仅可以用来管理窗口,还支持读取输入,处理事件等。
2.GLAD 提供OpenGL各平台底层库实现的函数调用位置查找功能
前面提到OpenGL是一个接口,每个平台有不同的实现库,那么在运行编译并不知道这些库的函数地址,那么GLAD库就是用来查找各平台库对应函数接口的位置,来让上层调用。
3.GLM 数学库
编写图形渲染代码中需要大量的使用到矩阵变换,向量变换以及各种针对矩阵和向量的计算。这一整套功能都由 GLM 库为我们提供
4.stb_image.h 文件图像加载库
能够加载大部分流行的文件格式,并且能够很简单得整合到你的工程之中,仅仅需要把这个头文件添加到你的项目中即可. 当我们需要给物体添加纹理、贴图(即现实中各种区分物品的表面可视的表面特征,金属、木头、墙、水、毛发、皮肤等)时,可用这个库来加载文件
GLSL: OpenGL Shading Language
用于编写OpenGL Shadingd代码的OpenGL专属编程语言
OpeGL渲染流程:
Vertex Data -> Vertex Shader -> Shape Assembly -> Geometry Shader -> Rasterization -> Fragment Shader -> Tests And Blending
顶点数据 -> 顶点着色器 -> 形状装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 测试与混合
顶点数据
通常以数组方式传递一组数据(组成物体的每个顶点的3D向量数据
{x,y,z, x1,y1,z2, …})
想象在纸上画几个点,然后用数字表示这些点距这张纸左下角的(水平方向)(垂直方向)多远
顶点着色器
对输入的3D坐标进行处理,如不同的坐标系统的坐标变换
想象你在一张A4纸上画了几个点后,想临摹到一张A2纸上,你可以把A4纸铺在A2纸上,两张纸可以左上角对左上角,右下角对右下角,右上角对右上角,左下角对左下角,或者中心对中心这几种方式。
把A4纸上的点画到A2纸上,此时这些点在A2纸上的点距离A2纸左下角的距离与A4纸这些点距离左下角的距离可能是不一样,因为两张纸大小不一样,A4纸放置在A2纸上的位置也有几种方式,因此最后描绘在A2纸上的点也有几种不同位置的结果。
另一个例子,一个人在新疆的乌鲁木齐,在不同的空间中确定他的位置:
基于中国: 在中国土地的左上方
基于地球: 在地球的上半部分中的下方
基于太阳系: 靠近太阳中心的第三颗星球
基于不同大小的空间,物体的位置会不断的发生变化,这就需要进行坐标变换.
这基于小物体在大空间中放置位置的不同,会在大空间中对小物体位置描述产生不同的结果,最后要得到一个确定的位置,顶点着色器可以进行这种不同空间位置的变换,达到你想要的效果。
形状装配
将上一步传入的顶点进行连线装配成具有形状的图元
我们想象在纸上画了几个点,那么我们现在再想象用笔在这几个点之间画线连起来,就是形状装配
几何着色器
在生成的图元形状范围内生成新顶点,并将新生成的顶点与图元进行组合生成新形状
上一个步骤中,我们画了线在点之间连成形状,如果我们突然想加多个三角形,我们可以在原原的某三条线段每条上画一个点,然后再将三个点连起来,新的点和线所组成的新形状是这个阶段来生成的.
光栅化
在刚刚生成的形状图元区域内生成对应于屏幕显示的像素填充,然后将屏幕不显示的区域中的物体部分裁剪掉
片段着色器
在刚刚形状内部填充的像素中,计算每个像素最终显示的颜色,这个每个像素的颜色最后是基于物体本来的颜色+不同光源照射影响混合计算最后生成的颜色。
测试和混合
最后这个阶段会进行物体之间的深度测试(即物体之间谁在前面和后面,遮挡与被遮挡),以及Alpha透明度测试并进行混合,最后输出的不一样的颜色透明度等其他测试。
Vertex Array Object - VAO 顶点数组对象
该顶点数组可用来保存VB0对象,即创建一次添加到数组中,即可保持对刚刚创建的VBO对象进行引用,不用每次需要时重复很多步骤来重新创建。
需要设置顶点属性配置,即指针指向起始位置,索引间隔
有几种保存方式,分为一对一,一对多
VAO[VBO1,VBO2,…]
VAO[VBO]
Vertex Buffer Object - VBO 顶点缓冲对象
在GPU内存(显存)中管理保存顶点数据的对象,把顶点数据保存到显存中通过VBO来管理读取,比经过CPU->Memory->GPU的读取效率速度要快很多,几乎能立即访问到数据.
Element Buffer Object - EBO OR Index Buffer Object - IBO 索引缓冲对象
在绘制形状时,很多情况下不同形状的顶点位置同一个是重复的,如果使用EBO来进行指定索引,那么我们只需要在数组中保存一份顶点数据就行。如果不使用EBO,程序会按我们定义好的的指针起始位置和数据间隔来读取数据,那么每个形状要单独保存一份顶点数据,会重复很多相同的顶点数据,这样会占用更多的显存.
Normalized Device Corrdinates - NDC
标准化设备坐标是指,在一个 X[-1,1] Y[-1,1] Z[-1,1]之间的一个坐标系空间范围中,我们输入的顶点数据会被转化成这个坐标系中相应的位置,如果在这个范围之外的数据,就会被抛弃掉,不被用在后面的屏幕空间中。
这里有对NDC的深入讨论,这块知识深入理解会在后面补充
Shader 着色器
着色器内部就是进行编写图像处理代码的地方
in、out关键字表示数据的输入、输出
数据结构:int、float、double、uint和bool
Uniform 着色器全局变量,可以被着色器程序(Shdaer Program)任意访问和改变变量值
Texture 纹理
纹理即我们真实世界上区别不同物体的一个外在表现,人的皮肤、钢铁的铁锈、木头的木纹、毛衣、牛仔裤的不同,这些物体的表面理解为每个物体的纹理.
纹理坐标: X[0,1] Y[0,1] 即[0,0] - [1,1]这个矩形范围内的坐标用来放置一个纹理,超出这个范围有几种方式来表现超出这个范围外的纹理.
两个关于材质制作在动画与游戏渲染中的使用和创作的资料,一位在顽皮狗和迪士尼工作过的大神级人物的:
访谈
公开演讲
Transformations 变换 线性代数知识
“direction(方向)” + “magnitude(大小)” = Vectors(向量)
向量是具有 “方向” 和 “大小” 的变量 形象的想象 从一个点 指向 另一个点 的一个箭头
数学中 v = {x, y, z} 用来表示一个3D空间中的向量
默认,向量是从原点{0, 0, 0}指向空间中的任意一个点, {x, y, z}则是所指向的点
Scalar is a single number, 标量是一个数字 可以当作是向量中的一个分量
向量与标量的运算:{x, y, z} + Scalar =
{ x + Scalar = new X,
y + Scalar = new Y,
z + Scalar = new Z }
向量有 加、减、乘运算和取反
取反 -> 向量取当前方向的相反
-{x, y, z} = {-x, -y, -z}
加、减 -> 两个向量的x,y, z分别加减:
{x1, y1, z1} + {x2, y2, z2} =
{ x1 + x2 = new X,
y1 + y2 = new Y,
z1 + z2 = new Z }
长度 -> 如何得到向量的长度:
高中学过的勾股定理(Pythagoras Theorem)来获取长度(length)
取向量{x, y, z}中的x, y
v | 表示向量长度, 就等于 开根号下 的 x平方 + y平方 |
垂直三角形的两直角边相加开平方根等于第三边斜边的长度 就取的了向量的长度
(博客渲染引擎无法渲染出数学公式= =!,只好先文字描述,虽然看文字很难理解,无法直观的理解,解决博客框架的渲染引擎后加上直观的数学公式)
乘 -> 点乘(Dot Product)、叉乘(Cross Product)为什么不是普通的相乘,因为普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的
点乘:
两向量的数乘结果再乘与两向量间夹角的余弦值
v⋅k = | v | ⋅ | k | ⋅cosθ |
那么点乘的意义在哪?让v和k向量取单位向量即为1那么
v⋅k = 1⋅1⋅cosθ = cosθ
v⋅k = cosθ
两向量相乘为cosθ, 因为cos0 = 1 | cos90 = 0, 那么就可以判断两向量是平行还是垂直 |
因为一些情况下,我们并不在意两向量的长度,而关注的是他们的方向,从而可以计算得出余弦值,再获得向量夹角,在计算光照时,有重要作用.
叉乘:
两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量
叉乘的作用即为获得一条正交于另外两个向量的新向量
矩阵
最重要的一个知识点到了,前面关于向量的知识点都是为这里做铺垫的.
那么矩阵的作用是用来干嘛的呢,就是用来变换向量的。可以将一个向量进行缩放、位移、旋转,以及如果同时进行前面这三个变换可以将前面三个组合。
那么就有变换矩阵、位移矩阵、旋转矩阵,以及组合三个变换的组合矩阵,最终得到一个你所需要的矩阵,再用向量与矩阵相乘,即可得到新向量。
一个知识点,矩阵相乘的顺序很重要,因为矩阵乘法不遵守交换律,先旋转再位移和先位移再旋转是不一样的结果的。
矩阵相乘顺序是从右往左,如果要先缩放,再位移,那么缩放矩阵要在最右手边再往左才是位移矩阵。
矩阵的加减乘的数学实现可以在这里阅读,这里就不再重新复述一次,写好多字很累呀!
而上面的这些数学公式及运算在GLM这个数学库中已经被实现,我们只需要使用这个库来进行运算即可.
Coordinate Systems 坐标系统
Local Space -> World Space -> ViewSpace -> Clip Space -> Screen Space
几个重要的坐标变换矩阵
Model Matrix | View Matrix | Projection Matrix |
那么整个流程:
Local Space -> [Model Matrix] -> World Space -> [View Matrix] -> View Space -> [Projection Matrix] -> Clip Space -> [ViewPort TransForm] -> Screen Space
在View Space -> [Projection Matrix] -> Clip Space 这个阶段中
Projection Martix有两种矩阵Orthographic Projection Matrix和Perspective Projection Matrix
即正射和透视两种矩阵,区别在于透视具有近大远小的处理,而正射则没有
一个顶点坐标经过 模型矩阵、观察矩阵和投影矩阵,最后就变换乘裁剪空间中的坐标
Vclip = Mprojection ⋅ Mview ⋅ Mmodel ⋅ Vlocal
光照和颜色
现实世界中的物体所呈现出来的外观颜色是 物体本身所固有的颜色 + 光照 = 混合后反射至我们眼睛所看到的颜色
因此,在OpenGL中,我们需要对物体颜色及所有能对物体进行照射的光源 进行光照数值计算 从而得到最后真实的颜色
Ambient Lighting 环境光照
在极端情况如凌晨,此时周围的亮度时最暗的时候,最暗时我们依然能隐约看到物体外轮廓,月光等一些微弱环境光源能提供最基础的光,让我们能看清楚周围的环境,这些光称为环境光照。
Diffuse Lighting 漫反射光照
物体表面粗糙程度不同都会反射各个角度照射在其上面的光,我们从各个角度会看到差不多的颜色,我们称这个为漫反射光,漫反射光能表现出这个物体表面的材质特征
Specular Lighting 镜面光照
物体的高光部分,我们视线投射在物体上,总有一小块区域的亮度时明显高于其他区域的,因此这块区域的颜色及亮度我们基于镜面光照来计算
法向量
法向量是一个垂直于顶点向量表面的单位向量,我们可以通过计算照射在物体表面的光线与法向量的夹角,从而计算出漫反射的最终颜色的反射系数大小
法向量变换到模型空间时,物体的缩放、位移、旋转都会影响到法向量,因为法向量仅代表一个方向,并没位置信息,不等比例缩放也会让法向量不垂直于片段表面,从而让光照计算无法得到正确结果。
因此,在转换过程中我们要使用专门的 Normal Matrix 法线矩阵 [模型矩阵左上角的逆矩阵的转置矩阵] 来进行变换,从而保证得到正确的法向量。
Materials 材质
材质用来表现世界中各种各样的物体,钢、陶瓷、木头对光的反射和他们纹理都是不同,所以需要材质来表现出每个物体的光线和纹理特征
上面提到的光照的内容中:环境光、漫反射、镜面光都是构成一个材质的一部分,因此一个材质有这么些内容构成
环境光、漫反射、镜面光、反光度系数、纹理
最后输出 result = ambient + diffuse + specular
光源
光源有以下几种:
平行光 - 太阳光
光线近似平行,并来自同一个方向
点光源 - 路灯、灯泡、火
光线向光源360度方向发射,随距离增加而亮度逐渐衰减至0
衰减公式
Fatt = 1.0 / Kc + Kl d + Kq d^2
Kc 常数项保持1,保证分母永远不比1小,随距离增加亮度不会变亮
Kl d 一次向乘于距离,让亮度呈线性衰减
Kq d^2 二次想乘于距离的平方,让亮度随距离呈指数衰减
聚光 - 手电筒
向一个方向一定半径内发射,也是随距离亮度逐渐衰减至0
通过 聚光灯的 Position 位置向量 与 片段位置向量 FragPosition 计算得到 光线距离(灯到物体可见的任意位置) LightDir
然后用聚光灯的方向向量LightDirection与光线距离LightDir点乘得到两者间的夹角,从而用得到的夹角判断是否在聚光灯的照射角度范围内,从而做出物体表面片段的光照计算