Fork me on GitHub

games101-3-Transformation

Scale缩放

默认以原点为中心缩放。

(1)均匀缩放

img

image-20230515161619367

(2)非均匀缩放

img

img->img

Reflection反射

img

image-20230515161742312

Shear Matric

img

image-20230515161856725

Rotate旋转

img

此处是2维旋转,默认以(0,0)为重心旋转,默认以逆时针旋转,也就是角度值为正。

img

以边长为1进行旋转矩阵推导:

image-20230515162027508

如果旋转-θ:

img

矩阵为旋转θ时旋转矩阵的转置,并且由于旋转-θ是旋转θ的逆变换,因此该矩阵还为旋转θ时旋转矩阵的逆矩阵(一个矩阵的转置等于其正交矩阵,则该矩阵为正交矩阵)

线性变换

image-20230515162329289

Translation平移

img

此处是先进性线性变换再平移。

image-20230515162355592

齐次坐标

引入齐次坐标是为了将所有变换都变成一个矩阵乘以一个向量的形式。

img

点的平移变换,点平移后成为新的点:

img

向量具有平移不变性:

img

可以通过齐次坐标的w验证:

img

一个点加上一个点的结果是这两个点的中点,原因如下,在w不等于0的情况下,(x,y,w)是二维点(x/w,y/w,1)。

Affine Transformation仿射变换

2维仿射变换最后一行才是(0,0,1),如下矩阵是先进行线性变换再进行平移变换。

img

缩放变换矩阵 旋转变换矩阵 平移变换矩阵
img img img
Inverse Transform逆变换

逆变换相当于乘以变换的逆矩阵。

image-20230515164100755
Composite Transform组合变换

复杂变换可以通过简单变换得到,变换的顺序很重要,如下先进行平移变换再进行旋转变换(默认围绕原点旋转)和先进行旋转变换再进行平移变换的结果不同。再次说明了矩阵乘法不能随意交换顺序。在c中cos和sin函数的参数都是弧度

(1)弧度转角度:角度 = 弧度 * (180.0f / PI)

(2)角度转弧度:弧度 = 角度 * (PI / 180.0f)

img

img

img

将变换变成矩阵乘法,相当于从右到左应用。也可以先计算左边所有矩阵得到一个矩阵。

img
变换分解

由于旋转默认是围绕原点旋转,如果想围绕任意点旋转,可以将变换按照如下方式分解:

img

三维齐次坐标

img

在w不等于0的情况下,(x,y,z,w)是三维点(x/w,y/w,z/w,1)。

三维仿射变换

如下矩阵是先进行线性变换再进行平移变换。

img

三维缩放

img

三维平移

img

三维绕轴旋转

imgimg

img

任意旋转都可由如下的欧拉角组合获得(还有一种四元数旋转法,便于做叉乘):

img

任意旋转还可以使用如下的旋转公式,a是角度,n是任意向量,如果向量方向相同起始点不同,旋转结果会不同,所以此处默认向量过原点:

img

games101-2-Linear_algebra

线性代数

向量基础知识

向量(矢量):方向和长度,不关心绝对开始位置,只要A和B的相对位置不变,平移后还是相同向量。

img

向量的长度:img

单位向量(只关心方向不关心长度),将向量的每一个分量除以长度,也叫向量的标准化:img

向量操作:平行四边形法则和三角形法则(三角形法则也可以适用于多个向量首尾相连)。向量相加是将每个对应的分量分别相加,相减同理。

image-20230510234841798

向量的代数表示:将向量放入直角坐标系,X、Y为相互垂直的两个单位向量。用该方法向量就可以用x、y两个数表示,如下向量用4、3表示。将向量放入坐标系可以更方便的计算向量的长度。如向量(4,3)默认是过原点的。

img

图形学上默认向量是列向量:img

列向量转置和向量的长度计算:img img

当把一个向量加/减/乘/除一个标量,可以简单的把向量的每个分量分别进行该运算。数学上是没有向量与标量相加运算的,但是很多线性代数的库都对它有支持。向量取反就是将每一个分量取反。

向量的点乘

img

两个向量的点乘结果是一个数,点乘可以快速计算两个向量的夹角。例如两个单位向量点乘的结果就是他们夹角的余弦。

img img

点乘的性质:

img

点乘的代数计算(对应元素相乘,最后将所有乘积相加):

img

点乘的作用:

(1)计算一个向量在另一个向量上的投影

img

​ b向量在a向量上的投影的方向为a向量的方向,投影的大小为||b||cosθ,所以b向量在a向量上的投影为||b||cosθa,cosθ通过a向量和b向量的点乘获得。

计算向量的投影可以对向量进行分解

img

(2)通过点乘结果的正负判断向量在前还是在后(方向基本一致,垂直或者相反),在图形学上通过点乘结果判断两个向量的接近程度。例如对于两个单位向量,如果这两个向量方向接近,则点乘结果接近1;如果方向垂直,点乘结果接近0;如果方向相反,点乘结果接近1.

img

向量的叉乘

img

两个向量的叉乘结果是一个新向量,该向量垂直于原来两个向量所在的平面。新向量的大小为img,新向量的方向通过右手螺旋定则确定。

向量叉乘的性质如下,注意一个向量叉乘自己得到的是长度为0的向量,而不是0。两个向量的叉乘可以得到一个三维空间的坐标系(右手坐标系x叉乘y是正z是右手坐标系,opengl使用右手坐标系):

image-20230510234944416

向量叉乘的代数运算:

image-20230510235008653

向量叉乘的作用:

(1)判断左右位置关系

如下在X、Y平面,判断向量b在向量a的左侧还是右侧。计算向量a叉乘向量b,如果结果为正,则向量b在向量a在左侧,即a-b是逆时针;如果结果为负,则向量b在向量a在右侧,即a-b是顺时针。

image-20230515155411695

(2)判断内外

如下三角形,ABC按照逆时针顺序输入。AB叉乘AP,结果为正,点p在AB左侧;BC叉乘BP,结果为正,点p在BC左侧;CA叉乘CP,结果为正,p在CA左侧;结论p在三角形内部。如果ABC按照顺时针顺序输入,如果p在三条边右侧,则p在三角形内部。所以不管顺时针还是逆时针输入,p如果在三角形内部,p必须在三条边的同一边。在三角形光栅化时,用该方法可以判断三角形覆盖哪些像素。如果叉乘结果为0,则自定义是否在三角形内部。

image-20230515155440122
在三维直角坐标系(右手坐标系)中进行向量分解
image-20230515155508640

以u为例,u为单位向量,长度为1,p在u的投影为:

img

矩阵

N*M的矩阵:N行M列的矩阵

数学上是没有矩阵与标量相加减的运算的,但是很多线性代数的库都对它有支持。矩阵与标量之间的加减就是将矩阵中每一个元素都与标量进行加减。矩阵与矩阵之间的加减就是两个矩阵对应元素的加减运算,所以总体的规则和与标量运算是差不多的,只不过在相同索引下的元素才能进行运算。

矩阵数乘:将矩阵中的每一个数都与n相乘

矩阵乘法:NM的矩阵与MP的矩阵相乘得到N*P的矩阵,结果矩阵元素Aij由第一个矩阵第i行乘以第二个矩阵第j列获得。

矩阵乘法的性质:

image-20230515155028337

矩阵与向量相乘,一般默认向量在右侧,矩阵列数与向量行数相同:

image-20230515155049820

矩阵的转置:

image-20230515155114282

单位矩阵和逆矩阵:

image-20230515155143338

原矩阵乘以单位矩阵,相当于不对原矩阵不做任何操作.

向量的点积和叉积可以转换成矩阵乘法(向量默认为列向量):

image-20230515155234507

gl函数流程图

1.清除

首先设置清除颜色缓冲区、深度缓冲区和模板缓冲区的值,这一步只是设置而并非真正的清除,每一个缓冲区的清除值都有默认值。之后调用glClear函数,通过设置参数,用上述设置的清除值清除指定的缓冲区。

img

2.纹理贴图

首先使用sgpugl_glGenTextures创建纹理对象,纹理对象的名称空间是无符号整数,纹理对象的名称仅限于在sgpugl_glGenTextures使用。sgpugl_glActiveTexture指定需要激活的纹理单元,之后的操作都在该纹理单元下进行(如果仅一个纹理单元默认为GL_TEXTURE0,可以指定多个纹理单元)。sgpugl_glBindTexture将纹理对象绑定在纹理目标上。如纹理对象texture1绑定在纹理单元GL_TEXTURE0下的纹理目标GL_TEXTURE_2D上。接下来的操作都是针对该纹理目标进行(一个纹理单元下可以有多个纹理目标)。sgpugl_glTexImage(12)D为该纹理目标指定纹理图片,生成纹理。sgpugl_glTexParameteri设置纹理参数,如环绕方式、过滤方式等。

在绘制前需要指定激活的纹理单元。采样器的类型识别纹理单元上的纹理目标。例如,采样器类型为的sampler2D,则采样器会在先前指定的纹理单元上选择纹理目标TEXTURE_2D,进而对纹理进行采样。

如下图是将三个纹理对象绑定在两个纹理单元下的三个纹理目标上,用其中一个纹理进行绘制的流程图。假如Draw前指定激活的纹理单元为GL_TEXTURE0,定位到红色区。假如着色器中采样器类型为的sampler2D,则定位到蓝色区域,找到对应纹理进行采样。

其中绿色区域的函数可以用指定的数据修改纹理目标指定区域的内容。

img

3.深度测试,多边形深度偏移

用1中的方法清除深度缓冲区,此处以默认值为例。在进行深度测试前必须先用sgpugl_glEnable启用深度测试功能。顶点X,Y坐标会从标准化设备坐标经过视口变换变成窗口坐标。z值根据sgpugl_glDepthRange(f)设置的范围也会从标准化设备坐标映射到窗口坐标,两个范围参数都限制在[0,1]。光栅化之后深度测试之前,如果较多片元的深度相差非常小,例如在相同的深度上绘制两个重叠的三角形,会发生z-Fighting。因此需要使用sgpugl_glEnable启用偏移,再通过sgpugl_glPolygonOffset对绘制的片元的深度值(由顶点插值得到)进行整体偏移。深度测试时通过sgpugl_glDepthFunc设置深度缓冲比较函数,默认为GL_LESS,即当前绘制的片元深度如果小于深度缓冲对应位置的深度值,则将颜色值写入颜色缓冲(之后可能被覆盖或者混合),否则丢弃。保留片元的深度值是否更到深度缓冲,通过glDepthMask设置。最后根据情况关闭深度测试和偏移。

img

4.颜色混合,Alpha测试

如果要绘制完全透明的物体,可以将全透明物体的Alpha值设置为0,将不透明物体的Alpha值设置为1。利用sgpugl_glEnable启用透明测试,透明测试在模板测试之前。sgpugl_glAlphaFunc可以指定透明度测试函数,此处以CL_GREATER,预设值为0.1为例。如果当前绘制的片元的Alpha为0,小于预设值,片元被丢弃。如果当前绘制的片元的Alpha为1,大于预设值,则保留片元。通过保留和丢弃片元来实现完全透明的效果。最后根据情况关闭透明测试。透明测试还常用于去除纹理图白色背景。

如果要绘制半透明物体,需要使用颜色混合(不透明物体直接颜色覆盖),该步骤在所有测试之后。绘制时要先绘制不透明物体(开启深度测试并允许深度写入)之后关闭深度写入,最后按照从远到近的顺序绘制半透明物体,不然可能会出现绘制错误。

情况1:半透明物体A在不透明物体B后

情况2:半透明物体A在不透明物体B前

情况3:半透明物体A在半透明物体B前

在情况1下,先绘制B再绘制A,且A不开启深度测试,不管A是否允许深度写入,A被遮挡的部分都会绘制出来。因此需要开启深度测试。在情况3下,先绘制A再绘制B,且A不禁止深度写入,B被遮挡的部分会被直接丢弃。因此需要禁止深度写入。在情况2下,假设先绘制A再绘制B,由于A禁止深度写入,B被遮挡的部分会绘制并覆盖A。因此需要先绘制不透明物体。在情况3下,先绘制A再绘制B,绘制B时,A为目标色B为源色;先绘制B再绘制A,绘制A时,B为目标色A为源色;根据混合算法的设置,这两次的结算结果可能不同。因此需要按照顺序绘制。

利用sgpugl_glEnable启用颜色混合,sgpugl_glBlendFunc可以指定颜色混合方式,即如何将深度测试保留的片元颜色值与颜色缓冲区对应的已存在的颜色值进行计算得到新的颜色值。最后根据情况关闭颜色混合或再次开启深度写入。

img

5.颜色填充

可以使用sgpugl_glColorPointer或者sgpugl_glColor设置顶点颜色数据,在使用sgpugl_glColorPointer前需先开启顶点颜色属性。但是sgpugl_glColorPointer优先级高于sgpugl_glColor,当使用sgpugl_glColorPointer时,不管sgpugl_glColor使用在sgpugl_glolorPointer前还是后,最后绘制出的颜色为顶点颜色数组定义的颜色。sgpugl_glColor可以对sgpugl_glVertexPointer定义的一组顶点数据设置统一的颜色,也可以在sgpugl_glBegin和sgpugl_glEnd中对每个顶点设置单独的颜色。sgpugl_glColorMask允许或禁⽌帧缓冲区某种通道的写⼊颜色缓冲。如果不允许R通道写入颜色缓冲,如果设置颜色为(1,0,1)紫色,最后绘制出来的会是(0,0,1)蓝色。注意如果着色模式不是平滑模式,则片元的颜色由图元的单个顶点得到。

img

6.模板测试

模板测试在深度测试之前。需要先清空模板缓冲区(清除值默认为0,每个值都为8位二进制,即有256种取值方式),并用sgpugl_glEnable启用模板测试。sgpugl_glStencilFunc设置模板测试的规则,如默认的“GL_ALWAYS,0, 0xFF”表示总是通过模板测试;下图中的“GL_NOTEQUAL,1,0xFF”表示如果片元对应模板缓冲的值不等于1,则通过模板测试。但是在比较之前,需要将模板缓冲的值和1分别与第三个参数0xFF进行与运算。sgpugl_glStencilOp设置如何更新模板缓冲,三个参数分别表示:模板测试失败时、模板测试通过但深度测试失败时及模板测试和深度测试都通过时采取的行为。如默认“GL_KEEP,GL_KEEP,GL_KEEP”表示无论测试结果如何,模板缓冲内的值都保持不变;下图中的“GL_KEEP,GL_KEEP,GL_REPLACE”表示当深度测试通过后,片元对应模板缓冲的值用sgpugl_glStencilFunc的第二个参数值代替。sgpugl_glStencilMask设置是否允许写入模板缓冲。如默认“0xFF”表示在写入模板缓冲之前,将写入的数和0xFF进行与运算(最后数保持原状,即允许写入)。如过设置“0x00”,写入的数会变成全0,即禁止写入,相当于sgpugl_glDepthMask(GL_FALSE)。也可以根据情况设置其他8位掩码。最后根据情况关闭模板测试。模板测试可用于绘制边框。

img

7.多边形正面判断,正背面剔除

在裁剪阶段,背面剔除用于绘制观察者所看到的面,不绘制看不到的面。这使得渲染的性能上可提高超过50%。gl按照如下方式区分图元的正面和背面:

正面:顶点连接顺序按照逆时针

背面:顶点连接顺序按照顺时针

Sgpugl_glFrontFace可以修改上述规则。注意顶点连接的顺序是相对于观察者而言的。因此图元是否需要剔除,是顶点连接顺序和观察者位置共同决定的。

img

8.渲染上下文的创建和管理

img

9.裁剪测试

裁剪测试在透明测试之前。先用sgpugl_glEnable启用裁剪测试,再用sgpugl_glScissor设置裁剪框,类似sgpugl_glViewport在窗口框出一块区域。但是sgpugl_glScissor是在裁剪框内的才进行绘制。sgpugl_glViewport相当于变相重置本次绘制的窗口,将所有需要绘制的物体映射在视口区域内。

img

10.顶点数据、矩阵变换、裁剪和视口

可以使用sgpugl_glVertexPointer或者在sgpugl_glBegin和sgpugl_glEnd中用sgpugl_glVertex设置顶点坐标数据,在使用sgpugl_glColorPointer前需先开启顶点坐标属性。

顶点首先经过世界变换(M)从模型空间变到世界空间,再经过观察变换(V)从世界空间变到观察空间,之后经过投影变换(P)从观察空间变到齐次裁剪空间。其中投影变换有正交投影和透视投影(近大远小)。上述变换需要对顶点坐标进行矩阵变换,在此之前需要先设置矩阵MV矩阵和P矩阵。可以用sgpugl_glLoadMatrixf自定义当前矩阵,也可以先通过sgpugl_glLoadIdentity将当前矩阵变成单位矩阵,再通过平移、旋转、缩放或者乘以其他矩阵得到最终需要的矩阵。但是调用draw前需要切换到MV空间下。

图元组装后进行裁剪,除了视锥体裁剪和背面剔除外,还可以通过sgpugl_glClipPlane设置裁剪平面进行图元裁剪,但是要先用sgpugl_glEnable启用裁剪平面。裁剪后对顶点坐标进行齐次除法即除以w使其变到NDC空间。最后通过视口变换将顶点坐标从NDC空间映射到视口区域的屏幕空间。

img

11.光照模型

使用光照模型需要提供顶点法线数据。物体的法线向量决定了它相对于光源的方向,从而计算顶点从光源接收的光线能量。可以使用sgpugl_glNormalPointer或者sgpugl_glNormal设置顶点法线数据,在使用sgpugl_glNormalPointer前需先开启顶点法线属性。sgpugl_glNormal可以对sgpugl_glVertexPointer定义的一组顶点数据设置统一的法线,也可以在sgpugl_glBegin和sgpugl_glEnd中对每个顶点设置单独的法线。

下一步开始使用光照模型。先用sgpugl_glEnable(GL_LIGHTING)开启整体光照效果。使用sgpugl_glLightfv设置光源,即设置入射光。光源最多设置8个,从GL_LIGHT0到GL_LIGHT7,每个光源可以设置的参数如下图。其中光源位置分为方向性光源(平行光)和位置性光源(点光源),聚光灯光源方向到光源的常数衰减因子这6个属性都是针对位置性光源。用sgpugl_glEnable开启光源,如sgpugl_glEnable(GL_LIGHT0)。接下来使用sgpugl_glMaterialfv设置光照模型的材质参数,即设置材质颜色(材质颜色和光照颜色作用形成出射光)。由于物体有两面,材质可能不同,因此要先确定为哪一面设置材质,可以设置的材质参数如下图。最后用sgpugl_glLightModelfv设置光照模型,便于进行光照计算。光照模型的概念由下图所示4个部分组成。

img

12.像素操作和帧缓冲区(更高版本的opengl基本不用这些)

(1)sgpugl_glCopyPixels进行像素的复制操作,从帧缓冲区复制到帧缓冲区,不涉及内存,因此避免了显存与内存间格式转换的问题,并且加快了运算速度。该函数前四个参数表示复制像素来源的矩形的左下角坐标、宽度和高度,可以当作是在窗口确认了一块矩形区域,第五个参数用于确认复制的内容,通常使用GL_COLOR,表示复制像素的颜色,但也可以是GL_DEPTH(深度缓冲数据)或GL_STENCIL(模板缓冲数据)。通过sgpugl_glRasterPos2f设置像素绘制的起始位置,系统会将该起始位置转换成窗口坐标,之后将会从起始位置绘制出刚刚窗口上指定的矩形中的内容。

(2)首先开辟一块内存空间。sgpugl_glReadPixels用于读取帧缓冲区的数据到内存,前四个参数同sgpugl_glCopyPixels,在窗口确认了一块矩形区域。第五个参数表示读取的内容,例如:GL_RGB就会依次读取像素的红、绿、蓝三种数据。第六个参数表示读取的内容保存到内存时所使用的格式,如GL_UNSIGNED_BYTE会把各种数据保存为GLubyte。第七个参数表示一个指针,即从帧缓冲区读取的数据保存在内存的位置。必要时可以使用sgpugl_glPixelStorei设置像素保存到内存时的对齐方式。通过sgpugl_glRasterPos2f设置像素绘制的起始位置(因此该函数比起sgpugl_glReadPixels少了第一个和第二个参数),sgpugl_glDrawPixels将从内存读取数据到帧缓冲区,从起始位置绘制出刚刚窗口上指定的矩形中的内容。该绘制方法比起sgpugl_glCopyPixels速度较慢,因为整个过程是从显存-内存-显存,并且涉及格式转换问题。

(3)sgpugl_glBitmap用于将位图数据绘制在屏幕,位图数据可以理解为0、1矩阵,为1则在屏幕的对应像素绘制、为0不绘制。

image-20230417093822003

​ 第(3)部分代码如下,glBitmap函数参数前两位定义了位图的宽、高,如图字符大小为12*8的方阵,每一行数据用8位16进制表示。注意位图数据总是按块存储,每块的位数总是8的倍数,但实际位图的宽并不一定使8的倍数。组成位图的位从位图的左下角开始画:首先画最底下的一行,然后是这行的上一行,依此类推。第三、四个参数指定了位图的原点,默认为左下角,向上和向右为正方向。第五、六个参数指定该在位图光栅化后光栅位置的增量,如下代码的意思是第二个字符F在第一个字符F的基础上分别向X正轴和Y正轴移动20个像素单位。 (不过试的时候发现glRasterPos2i(0, 0)才显示,glRasterPos2i(20, 20)不显示,所以不太清楚这里的坐标系)

image-20230427202027799

1
2
3
4
5
6
7
8
9
GLubyte rasters[12] = {
0xc0, 0xc0, 0xc0, 0xc0, 0xc0, 0xfc,
0xfc, 0xc0, 0xc0, 0xc0, 0xff, 0xff};
glColor3f(1.0, 0.0, 0.0);//必须放RasterPos2i前
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
//光栅位置坐标与glVertex()提供的坐标同样对待,位于视口以外的点的当前光栅位置无效
glRasterPos2i(0, 0);
glBitmap(8, 12, 0.0, 0.0, 20.0, 20.0, rasters);
glBitmap(8, 12, 0.0, 0.0, 0.0,0.0, rasters);

​ 不过位图的宽高和实际给的位图数据也可能不同。

image-20230427203459953

13.图元绘制

首先确定绘制的图元类型,注意gpugl_glDrawArrays和sgpugl_glBegin的图元参数不同。

图元类型 gpugl_glDrawArrays是否有该参数 sgpugl_glBegin是否有该参数 流程图中对应类型
GL_POINTS
GL_LINES 线一类
GL_LINE_LOOP 线一类
GL_LINE_STRIP 线一类
GL_TRIANGLES 三角形一类/多边形
GL_TRIANGLE_STRIP 三角形一类/多边形
GL_TRIANGLE_FAN 三角形一类/多边形
GL_POLYGON × 三角形一类/多边形
GL_QUADS × 三角形一类/多边形
GL_QUAD_STRIP × 三角形一类/多边形

如果是绘制点可以用sgpugl_glPointSize(f)设置光栅化时一个点的直径,默认为1。如果绘制线,可以用sgpugl_glLineWidth设置直线宽度,默认为1。

直线可以用点画模式进行绘制。首先通过sgpugl_glEnable开启点画模式。sgpugl_glLineStipple设置点画的方式,其参数是由0、1组成的16位模式序列和重复因子,从模式低位开始,如果模式中的位是1,直线中对应像素就被绘制,否则就不绘制(如果直线长度大于16位则不断重复上述规则)。重复因子用来扩展模式,如模式中如果出现3个1会被扩展成个6个1。如果没有启用点画线功能,模式默认为OxFFFF、重复因子为1。多边形也可以用点画模式进行绘制。首先通过sgpugl_glEnable开启点画模式。sgpugl_glPolygonStipple设置点画的方式,其参数是一个指向3232的位图指针,可以理解为3232的0、1矩阵,通过GLubyte数组自定义(128个2位16进制序列)。从每个序列低位开始,模式中的位是1,多边形中对应像素就被绘制,否则就不绘制(如果多边形像素大于32*32则不断重复上述规则)。

sgpugl_glShadeModel用于设置平滑着色或者恒定着色。注意除了GL_POLYGON以外,恒定着色图元中片元的颜色为图元最后一个顶点的颜色,而GL_POLYGON下图元中片元的颜色为图元第一个顶点的颜色。

设置完上述所有后即可调用绘制函数,注意sgpugl_glDrawElements除了需要顶点数据以外,还需要索引数据。

img

X11窗口、glfw和链接

文件区分

​ glut跨平台窗口库;glfw跨平台窗口库(glut替代版);glut和glfw封装了Window的窗口管理系统,linux的窗口管理系统和创建上下文等(这些操作在每个系统上不同,所以该文件将其封装使得我们可以直接使用)。它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入。

GL/gl.h是基本的OpenGL标头,可提供OpenGL-1.1函数和令牌声明,甚至更多。对于超过1.1版的任何内容,都必须使用OpenGL扩展机制。由于这是一项枯燥而乏味的任务,因此GLEW项目已将其自动化,该项目将所有细节打包在一个易于使用的库中。该库的声明位于头文件GL/glew.h中,该文件隐含了常规的OpenGL标头,因此在包含GL/glew.h时,不再需要包含GL/gl.h,包含该头文件可以使用gl,glu,glext,wgl,glx里的全部函数。glad.h与glew.h作用相同,可以看作其升级版。这两个头文件使用时要放在glfw3.h 或者glut.h文件之前。gl3w.h和glad.h作用类似。

glfw和上下文

​ 除了窗口创建,glfw还封装了上下文创建。glfw可以用elg和glx创建上下文,egl可以支持gl、es和vukan,glx支持gl扩展支持es。glfw系统库为高版本3.3.2,可以在应用程序中对glfw进行配置。如下代码希望在运行gl程序使用glx的上下文,在运行es程序使用egl的上下文。其实在glfwinit的时候已经默认使用glx的上下文,支持gl和es了,所以在if(casetype==”gl”)后不需要显示指定,也可以保证在运行gl程序使用glx的上下文,并且支持es。不过为了使得运行es程序使用egl的上下文,需要设置GLFW_CONTEXT_CREATION_API和GLFW_CLIENT_API,注释如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if( !glfwInit() )
{
fprintf( stderr, "Failed to initialize GLFW\n" );
getchar();
return -1;
}
if(casetype=="gl"){
//采样
glfwWindowHint(GLFW_SAMPLES, 4);
//版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
//opengl模式
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
//用户不能调节窗口大小
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
}else{
glfwWindowHint(GLFW_SAMPLES, 16);
//上下文指定egl
glfwWindowHint(GLFW_CONTEXT_CREATION_API, GLFW_EGL_CONTEXT_API);
//API使用opengl es
glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
}

​ GLFW_OPENGL_PROFILE用于指定opengl模式,opengl es无该模式。opengl模式有COMPAT和COREl两种,CORE抛弃了部分用法,如果想兼容,使用COMPAT会少很多问题。

​ 在如上代码中,如果程序以gl-es-gl的顺序运行,在运行到es时,进入else使用egl创建上下文,调用es的API;之后运行最后一个gl时,进入if,由于if中没有显示指定上下文和调用的API,因此还是会使用egl创建上下文,调用es的API。

​ 在低版本的glfw文件中,链接的不是系统库而是用户自己指定的库,不能在应用程序设置,而是通过Cmakelists.txt设置,如下,通过ON和OFF设置是否使用egl创建上下文。

image-20230412162211136

​ egl用于关联原生窗口,创建上下文,通过eglBindAPI指定使用gl还是es,与平台无关(窗口-egl-API)。

1
2
eglBindAPI(EGL_OPENGL_ES_API);//绑定ES的API
eglBindAPI(EGL_OPENGL_API);//绑定GL的API

X11窗口

1、在linux中,“X11”指的是“X Window System”,是图形化窗口管理系统。因此调用X11使得窗口可以在linux上运行,不能在windows上运行。

2、使用API时,gl.h头文件中只有函数声明,需要链接到函数实现,glfw已经封装了函数链接,如果直接用X11创建窗口需要在Makefile手动链接。

1
LD_LIBS=-lX11 -lXext -lEGL -lGL //链接GL

3、X11窗口创建的部分函数和窗口监听

​ 创建窗口前先打开与server 的连接。在程序可以使用display 之前,必须先建立一个和X server 的连接。这个连接建立以后,就可以使用Xlib 的函数或宏定义来获得display 的信息了。当参数设置为NULL时,为默认的display环境变量。这个函数返回一个指向display类型结构的指针,表明与X server建立了连接,并且包含了X server的所有信息,可以使用display之上所有窗口。

1
Display *display = XOpenDisplay(NULL);

​ 在进行了一些配置参数配置后,使用如下两个函数创建窗口(参数含义以及创建窗口前的配置流程暂时未知)。函数返回创建的窗口的ID,并使得X server产生一个CreateNotify 事件。

1
2
XCreateWindow
XCreateSimpleWindow

​ X是一个服务器–客户端的结构。由服务器向客户端发送事件信息,让客户端知道发生了什么事情,然后客户端告诉服务器它感兴趣的是什么事情,也就是说,客户端会对那些事件产生反应。如下函数用于客户端告诉服务器窗口会对哪些事情有响应。StructureNotifyMask 即改变窗口状态,比如尺寸,位置等,对应事件ConfigureNotify;ExposureMask 对应事件Expose ;KeyPressMask 即键盘响应对应事件KeyPress。

1
XSelectInput(display, win,ExposureMask|StructureNotifyMask);

​ 创建窗口之后,窗口并不能显示出来,需要调用如下函数来 画窗口让它显示。如果这个窗口有父窗口,那么在所有父窗口没有画出来之前,这个窗口即使用了这个函数,也是不能显示出来的。必须等所有父窗口都显示了,这个窗口才能画。X server产生一个MapNotify事件。客户端已经有相应操作了,绘制窗口。

1
2
XStoreName (display, win, "gears");//设置窗口名
XMapWindow(display, win);

​ 进入窗口事件循环,获得事件,处理或丢弃。接收到的事件由XNextEvent 函数从消息队列里获得,把事件放到event.type 里并从队列里删除该消息 。当队列为空也就是没有下一个事件被接收时,程序就一直停留在XNextEvent里直到有下一个事件,无法执行之后的步骤。因此可以使用XPending(display)在有事件的时候响应事件,没有事件的时候在窗口上绘制。如果窗口的信息改变了,就需要XFlush 函数让窗口重画,但XNextEvent函数会隐式地调用XFlush。在opengl绘制函数的部分,glClearColor设置窗口颜色后,需要用

​ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

​ 用上述指定颜色初始化颜色缓存,使用默认值初始化深度缓存,再利用函数

​ eglSwapBuffers(mContext.eglDisplay, mContext.eglSurface);才能显示在窗口(此处以egl配置上下文为例)。

1
2
3
4
5
6
7
8
9
10
11
XEvent myevent;
while (1) {
if (XPending(display)) {
XNextEvent(display, &myevent);
switch(myevent.type) {
case ClientMessage:{
}
case ConfigureNotify:{
}}}
/*opengl绘制函数*/
}

​ 关闭窗口事件(若窗口关闭则打印信息)

//while循环外
Atom deleteAtom = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display, win, &deleteAtom, 1);
//while循环内
 case ClientMessage:{
       if ((unsigned)myevent.xclient.data.l[0] == deleteAtom){
           printf("X WINDOW DELETED\n");
           return 0;
       }
   }

​ 窗口改变事件

​ 如下函数通过代码人为改变并重新设置窗口,因此一直存在ConfigureNotify事件。

const unsigned int mask = CWWidth | CWHeight;
XWindowChanges changes;
int newWidth = 128, newHeight = 128;
changes.width = newWidth;
changes.height = newHeight;
XConfigureWindow(display, win, mask, &changes);

​ 如下函数获取由外部事件对窗口产生的变化并改变viewport,使得绘制的图像根据窗口的变化变化,在外部事件发生时才存在ConfigureNotify事件。

case ConfigureNotify:{
    reshape(myevent.xconfigure.width,myevent.xconfigure.height); 
    break;
}

​ Expose 事件可能用于绘制和显示,在opengl绘制部分外部可以添加case Expose 。

​ 退出窗口时,需要关闭和X server 的连接,于是也就销毁了相关资源,关闭了窗口。

1
2
XDestroyWindow(display, win);
XCloseDisplay(display);

参考链接:

https://blog.csdn.net/rufanchen_/article/details/7640584

https://www.cnblogs.com/okgogo2000/p/4322753.html

源文件->可执行文件过程

1.预处理:包括删除注释、宏扩展、#include文件包含等。预处理后生成.i文件。

2.编译:由编译器完成,检查语法及语义,生成错误或警告。编译后生成汇编语言.s文件。

3.汇编:将汇编文件翻译成机器码指令(二进制),将指令打包形成.o目标文件。

4.链接:完成调用的各种函数、静态库和动态库的链接,从函数原型链接到函数实现。形成.exe文件。

Linux平台下cmake和make的区别

Makefile编译规则文件。

make通过Makefile文件自动化批量处理编译,将源文件变成最终的可执行文件(包含gcc的功能)。

不同平台的Makefile文件不同,cmake作为跨平台编译工具可以将CMakeLists.txt文件转化为所需要的Makefile文件

Cmake

window平台下利用Cmake软件代替Linux下的cmake命令,用编译器如vs代替Linux下的make命令。

Cmake软件将cmake分为两步:

1.Configure:配置,生成需要的文件夹及准备文件

2.Generate:根据CMakeLists.txt生成工程文件。如果编译器选择vs,则生成vs工程文件,如果是Linux,则生成Makefile。生成vs工程文件,同Linux生成Makefile文件,都是规定了编译规则。

最后由vs执行工程,即执行make。

动态库和静态库

1.区别

​ 使用封装好的库函数十分方便并且十分高效,库分为静态库和动态库。

库类型 windows linux
静态库 .lib .a
动态库 .dll .so

​ 当程序与静态库链接时,静态库中所包含的所有函数方法都会被拷贝到最终的可执行文件中去。这就会导致最终生成的可执行代码量相对变多,相当于编译器将代码补充完整了。编译后的可执行程序不需要静态库,因为所有使用的函数都已经被编译进去了。这种方式会让程序运行起来相对快一些,不过也会有个缺点: 占用磁盘和内存空间,导致可执行程序过大。另外,静态库会被添加到和它链接的每个程序中去, 而且这些程序运行时, 都会被加载到内存中,无形中又多消耗了更多的内存空间。 与动态库链接的可执行文件只包含它需要的函数方法的引用表,而不是所有的函数代码,只有在程序执行时, 那些需要的函数代码才会被拷贝到内存中。这样就使可执行文件比较小, 节省磁盘空间,更进一步,操作系统使用虚拟内存,使得一份动态库驻留在内存中被多个程序使用,也同时节约了内存。不过由于运行时要去链接库会花费一定的时间,执行速度相对会慢一些。如果要修改的刚好是库函数的话,在接口不变的前提下,使用动态库的程序只需要将动态库重新编译就可以了,而使用静态库的程序则需要将静态库重新编译好后,将程序再重新编译一遍。

2.两种链接

​ 现在源程序main.cpp链接库sub,sub链接库add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/*add.h */
#ifndef _ADD_H_
#define _ADD_H_
void add();
#endif
------------------------------------------------------------
/*add.c*/
#include "add.h"
void add()
{
printf("add\n");
}
------------------------------------------------------------
/*sub.h*/v
#ifndef _SUB_H_
#define _SUB_H_
void sub();
#endif
----------------------------------------------------------
/*sub.c*/
#include "add.h"
#include "sub.h"
void sub()
{
printf("sub\n");
add();
}
------------------------------------------------------------
/*main.c*/
#include "sub.h"
void main()
{
sub();
}
1
2
gcc -c add.c
gcc -c sub.c

​ 首先生成的文件:sub.o ,add.o,无论是静态库文件还是动态库文件,都是由 .o 文件创建。使用-c不会链接、生成可执行文件,只会预处理、编译和汇编。

(1)静态库

1
2
ar cr libadd.a add.o
ar cr libsub.a sub.o

​ ar:静态函数库创建的命令

​ -c :create创建

​ 库文件的命名规范是以lib开头(前缀),紧接着是静态库名,以 .a 为后缀名。

1
gcc -o main main.c -L . –l sub -L . –l add

​ -L :指定函数库查找的位置,’.’表示在当前目录下查找

​ -l:指定函数库名,其中的lib和.a(.so)省略。

​ -o:链接,生成可执行文件,指名生成的可执行文件名

​ 使用静态库内部函数,只需要在使用到这些函数的源程序中包含这些函数的原型声明(头文件),然后在用gcc命令生成可执行文件时指明静态库名,gcc将会从静态库中将函数拷贝到可执行文件中。gcc会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件。在程序main.c中,包含了静态库的头文件sub.h,然后在主程序main中直接调用函数sub()即可。生成可执行文件后静态库可以不再需要。

​ 注意:

1
2
gcc -o main main.c -L . –l sub 
gcc -o main main.c -L . –l add -L . –l sub

​ 都会发生sub无法链接到add导致sub中的函数undefined reference to,因此链接顺序不能变,而且用main链接sub和add即可,不需要单独写一行sub链接add。

(2)动态库

1
2
3
4
gcc -fPIC -o add.o -c add.c
gcc -fPIC -o sub.o -c sub.c
gcc -shared -o libadd.so add.o
gcc -shared -o libsub.so sub.o
     -fpic:产生代码位置无关代码,则产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。

​ -shared :生成共享库

1
gcc -o main main.c -L . –l sub -L . –l add

​ ./执行时会发生如下报错:

     error while loading shared libraries:libmymath.so: cannot open shared object file: No such file or directory

​ 原因如下:

​ gcc -o 链接器工作于链接阶段,工作时需要-l指定库名和-L指定库路径。如果在指定库路径没有找到库,会发生

undefined reference to的报错,导致可执行文件无法生成。但是有时不会发生该报错,可以对make添加-WL -Z DEFS编译选项,在生成可执行文件前找到该错误。

​ ./运行可执行程序时还需要动态链接器,工作于程序运行阶段,工作时需要提供动态库所在目录位置。该目录与上述-L指定的路径不同,动态链接器去固定位置(通过环境变量)查找动态库,如果找不到就会报错,所以需要先将动态库的工作目录加入到环境变量中。一般会去默认的动态库搜索路径/usr/lib查找,可以将.so文件复制到/usr/lib中来解决上述报错。

其他

1
$ ldd exe 查看可执行程序运行时链接的动态库

​ 如果在源文件的头文件中使用

1
#include<GLFW/glfw3.h>

​ 则之后会默认链接到/usr/lib下的系统库(库名=>目录)

image-20230412085632785

​ 在项目文件中添加一个新文件夹,使用自定义的glfw库文件。

image-20230412090145388

​ 头文件会链接到自己的库文件而不是系统的库文件。编译源文件时,需要将glfw文件一起编译,因此需要修改源文件的Cmakelist.txt文件。

1
add_subdirectory(external) 

​ cmake会通过源文件的Cmakelist.txt找到external目录下的Cmakelist.txt

1
include_directories(external/glfw-3.1.2/include/GLFW)

​ 向工程添加多个特定的头文件搜索路径,添加该行后源文件的头文件部分可以改成

1
#include<glfw3.h>

​ 如果包含头文件后,没有找到external目录下的Cmakelist.txt,无法生成自己的库文件,最后还是会链接到系统库。如果glfw文件编译链接成功,最后就会链接到自己的库。在Cmakelist.txt或者其他文件中指明了动态链接库的搜索位置,因此不会去默认的路径/usr/lib。

image-20230412091529196

gdb调试

image-20230412091902342

​ 可以用如下方式调试:

1
2
3
4
5
$ gdb 可执行程序名
(gdb) b 函数名 //设置断点,运行到该函数暂停
(gdb) run //运行到第一个断点暂停
(gdb) c //继续运行到下一个断点暂停
(gdb) quit //退出gdb环境

计算着色器

计算着色器是在GPU上运行的,是在普通渲染管线之外的着色器程序。计算着色器是完全用于计算任意信息的着色器阶段。虽然它可以进行渲染,但它通常用于与绘制三角形和像素没有直接关系的任务。利用GUP的快速计算和并行性,可以用于处理大量的计算。

opengl4.3才开始支持计算着色器,opengl es是3.1才开始支持计算着色器。

1
2
3
4
5
6
7
8
9
10
11
12
13
#version 320 es
layout (local_size_x = 1) in;
layout (rgba32f, binding = 0) uniform readonly mediump imageBuffer po_buffer;
layout (rgba32f, binding = 1) uniform writeonly mediump imageBuffer po_buffer1;
void main()
{
//此处全局工作组和本地工作组yz均为1,因此可以通过x确定位置
int pos = int(gl_GlobalInvocationID.x);
vec4 value = imageLoad(po_buffer, pos);
value.x = value.x+0.4;
value.y = value.y+0.4;
imageStore(po_buffer1, pos, value);
};

binding与之后glBindImageTexture的第一个参数相同,通过ID绑定对应的tbo,而tbo绑定对应的buffer。相当于通过绑定知道从哪个buffer读取数据,从哪个buffer写入数据。因为此处是读取buffer所以使用imageBuffer,如果是数组的话可能使用image2D。es需要指定精度,所以添加mediump。同时因为需要读取的是rgba而不是r(如果只设置r,value就无法读取y值),所以需要指定readonly和writeonly。但是在opengl4.3中不需要指定精度,并且读写可以在一个buffer中进行。

imageLoad从buffer的指定位置读取数据,imageStore将值写入buffer的指定位置。

计算着色器任务以组为单位进行执行,称为工作组。拥有邻居的工作组被称为本地工作组(局部), 这些组可以组成更大的组,称为全局工作组,而全局工作组通常作为执行命令的一个单位。计算着色器会被全局工作组中每一个本地工作组中的每一个单元调用一次,工作组的每一个单元称为工作项,每一次调用称为一次执行。执行的单元之间可以u通过变量和显存进行通信(不同工作组不能通信),且可执行同步操作保持一致性(单个工作组内可以并行)。

如下例子假设全局工作组yz轴都为1 ,局部工作组只有xy:

gl_LocalInvocationID.x × local_size_y+gl_LocalInvocationID.y+local_size_x×local_size_y×gl_WorkGroupID.x=gl_GlobalInvocationID.x×local_size_y×y轴的工作组个数+gl_GlobalInvocationID.y

local_size是本地工作组的大小,有xyz三个维度,不设置默认为1。gl_WorkGroupID本地工作组在全局工作组的索引(从0开始),local_size_x×local_size_y本地工作组的大小(该大小尽量与硬件匹配),gl_LocalInvocationID工作项在本地工作组的坐标(从0开始)。gl_GlobalInvocationID:在全局工作组中,当前工作项所在位置。(在es中imageLoad后的pos只能使用int,所以可以设置本地工作组和全局工作组yz均为1,这样可以只使用gl_GlobalInvocationID.x就可以遍历到所有数据,否则可以通过上述公式计算。)本地工作组的大小×全局工作组的大小->所有需要遍历的数据

与普通着色器一样正常链接

1
2
3
4
5
6
7
8
computeShader = LoadShader(GL_COMPUTE_SHADER, cShaderStr);
compute_prog = glCreateProgram();
if (compute_prog == 0)
{
return 0;
}
glAttachShader(compute_prog, computeShader);
glLinkProgram(compute_prog);

将顶点数据arraybuffer与texbuffer绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
glGenBuffers(1, &po_buffer);
glBindBuffer(GL_ARRAY_BUFFER, po_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices), vVertices, GL_DYNAMIC_COPY);

glGenBuffers(1, &po_buffer1);
glBindBuffer(GL_ARRAY_BUFFER, po_buffer1);
glBufferData(GL_ARRAY_BUFFER, sizeof(vVertices),vVertices, GL_DYNAMIC_COPY);

glGenTextures(1, &po_tbo);
glBindTexture(GL_TEXTURE_BUFFER, po_tbo);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, po_buffer);

glGenTextures(1, &po_tbo1);
glBindTexture(GL_TEXTURE_BUFFER, po_tbo1);
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGBA32F, po_buffer1);

先使用计算着色器,绑定tbo和id,GL_READ_WRITE设置读写权限(如果计算着色器规定了这里可以就设置读写,opengl因为读写可以在一个buffer进行因此一定要设置可读可写)。glDispatchCompute的三个参数设置了全局工作组的大小,三个参数可以理解为xyz方向各有几个本地工作组。glMemoryBarrier是隔断作用,为了保证计算着色器中纹理像素全部写入完成才进行下一步。最后使用渲染着色器进行绘制。如果至使用一次计算着色器的话,可以不用vao,渲染着色器默认使用writeonly的buffer。

1
2
3
4
5
6
glUseProgram(userData->compute_prog);
glBindImageTexture(0, po_tbo, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
glBindImageTexture(1, po_tbo1, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
glDispatchCompute(3, 1, 1);
glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);
vao(po_tbo1配置数据与tbo1绑定)

如果要进行类似transformfeedback的操作,进行累加变换,需要交换读写tbo。

1
2
3
4
5
6
7
8
9
if(flag){
glBindImageTexture(0, po_tbo, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
glBindImageTexture(1, po_tbo1, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
vao(po_tbo配置数据与tbo绑定)
}else{
glBindImageTexture(1, po_tbo, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
glBindImageTexture(0, po_tbo1, 0, GL_FALSE, 0, GL_READ_WRITE, GL_RGBA32F);
vao1(po_tbo1配置数据与tbo1绑定)
}

webgl基础

1.导入相关文件

1
2
3
<script src="../lib/webgl-utils.js"></script>
<script src="../lib/webgl-debug.js"></script>
<script src="../lib/cuon-utils.js"></script>

2.在html元素中创建Canvas画布,并且对其css进行相关设置。该画布具有2D绘图和3D绘图的功能。

1
<canvas id="webgl"></canvas>

3.通过JavaScript获取上面创建的Canvas元素返回一个Canvas对象。Canvas对象也可以不通过标签创建,然后id方式获取,也可以通过DOM直接创建。

1
2
3
4
5
//获取
var canvas= document.getElementById('webgl')
//直接创建后添加
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);

4.获取Canvas上下文,执行canvas.getContext('2d')返回对象具有一系列绘制二维图形的方法,比如绘制直线、圆弧等API。执行canvas.getContext('webgl');返回对象具有一系列绘制渲染三维场景的方法,也就是WebGL API。

1
2
var c =canvas.getContext('2d');
var gl=canvas.getContext('webgl');

5.编写着色器文件

可以直接将着色器文件写成字符串

1
2
3
4
5
var vertexShaderSource = '' +    
'void main(){' +
' gl_PointSize=20.0;' +
' gl_Position =vec4(0.0,0.0,0.0,1.0);' +
'}';

或者将着色器文件写在

请我喝杯咖啡吧~

支付宝
微信