基础光照
冯氏光照模型(Phong Lighting Model)由3个分量组成:环境(Ambient)、漫反射(Diffuse)、镜面(Specular)光照。
观察空间,是以摄像机的视角作为场景原点时场景中所有的顶点坐标;要定义一个摄像机,需要它在世界空间中的位置、观察方向、指向它右侧的向量及指向它上方的向量。
1.摄像机位置
摄像机移动方向应与物体移动方向相反;
2.摄像机方向
摄像机方向向量指从它到目标向量的相反方向;
3.右轴
代表摄像机空间的x轴的正向;通过先定义一个上向量,将其与方向向量叉乘。即可得右向量。
4.上轴
将右向量和方向向量进行叉乘;
若有三个相互垂直的轴定义了一个坐标空间,即可使用这三个轴加一个平移向量创建一个矩阵,该矩阵乘以任何向量可以将其变换到该坐标空间中;故可以创建LookAt矩阵:
其中R为右向量,U为上向量,D为方向向量,P为摄像机位置向量。
通过定义摄像机位置,目标位置和表示世界空间中的上向量的向量(用于计算右向量使用的上向量)即可计算LookAt矩阵。
|
|
设置了相机位置,相机朝向和上方向之后,可以通过按键对相机位置进行修改,从而调整观察角度;
设置通过按键输入调整摄像机的位置;
|
|
纹理坐标,与每个顶点关联,表示该从纹理图像的哪个部分采样,之后在图形的其他片段上进行片段插值;纹理坐标在x和y轴上,范围为0到1(2D纹理图像),纹理坐标起始于(0,0),即纹理图片的左下角,终于(1,1),即纹理图片的右上角。
将纹理坐标设置在范围(0,1)之外,OpenGL的处理方式:
|
|
若选择GL_CLAMP_TO_BORDER选项,需要指定边缘的颜色:
|
|
即一系列的纹理图像,后一个纹理图像是前一个的二分之一;距观察者的距离超过一定阈值时,OpenGL会使用不同的多级渐远纹理;
1. stb_image.h
单头文件图像加载库,可以加载大部分流行的文件格式,并能简单的整合到工程中。将stb_image.h文件加入工程,并另创建一个新的C++文件,输入代码:
通过定义STB_IMAGE_IMPLEMENTATION,预处理器会修改头文件,让其只包含相关的函数定义源码,相当于将这个头文件变为一个.cpp文件。
函数读取图像文件;
函数输入生成纹理的数量,将其存在第二个参数的数组中(此处为单独的一个unsigned int),并绑定该纹理对象;
纹理通过glTexImage2D来生成,第一个参数指定了纹理目标;第二个参数为纹理指定多级渐远纹理的级别,0表示基本级别;第三个参数表示希望将纹理存储为何种格式;下个参数应始终设为0;第七第八个参数定义了源图的格式和数据类型;最后一个参数为图像数据;
调用glTexImage2D时,当前绑定的纹理对象会被附加上纹理图像,目前只有基本级别的纹理图像被加载,若要使用多级渐远纹理,必须手动设置所有不同的图像(不断递增第二个参数),或者直接在生成纹理之后调用glGenerateMipmap,这会为当前绑定的纹理自动生成所需要的多级渐远纹理。
释放图像的内存;
GLSL有一个供纹理对象使用的内奸数据类型,称为采样器,以纹理类型为后缀,可以简单声明一个uniform sampler*D把一个纹理添加到片段着色器中,之后将纹理赋值给该uniform。GLSL内建的texture函数来采样纹理的颜色,第一个参数是纹理采样器,第二个参数是对应的纹理坐标;texture函数会使用之前设置的纹理参数相应的颜色值进行采样,这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
在调用glDrawElements之前绑定纹理,会自动把纹理赋值给片段着色器的采样器;
一个纹理的位置值通常称为一个纹理单元,一个纹理单元的默认纹理单元是0,是默认的激活纹理单元,故不需要使用glUniformi进行位置分配。
纹理单元的主要目的是在着色器中使用多于一个的纹理,通过把纹理单元赋值给采样器,可以一次绑定多个纹理,只要首先激活对应的纹理单元。
使用函数告知OpenGL每个着色器采样器属于哪个纹理单元,放在渲染循环之前进行;
在绑定纹理之前先激活纹理单元;
齐次坐标中,第四个分量为1,表示坐标点;第四个分量为0,表示向量;
|
|
定义4维矩阵;
用此handle来获取shader中的“世界”平移变换矩阵一致变量的引用;
设置物体在x轴上-1到1之间来回滑动;
使用glUniform*函数将数据加载到shader一致变量中,第一个参数是一致变量的位置(在shader编译后使用glGetUniformLocation()获取);第二个参数是要更新的矩阵的个数;第三个参数指行优先还是列优先;第四个参数表示内存中矩阵的起始地址;
OpenGL是列主序的,故使用GLM记录矩阵时,由于GLM使用列主序,因此需要设置为GL_FALSE;
定义了一个4x4的矩阵一致变量;由于顶点缓冲区中三角形的顶点位置是一些3维的向量,故需要在顶点着色器中添加第四个分量;
绕z轴逆时针旋转矩阵计算:
绕y轴逆时针旋转矩阵计算:
绕x轴逆时针旋转矩阵计算:
这里借助GLM库对矩阵进行存储;
需要的GLM的大多数功能都可以在下面三个头文件中找到;
将单位矩阵和位移向量传递给glm::translate函数来构造平移矩阵;
将物体沿每个轴缩放到0.5倍,再沿z轴旋转90度,GLM希望它的角度是弧度制的,故使用glm::radians将角度转化为弧度;由于将矩阵传递给了GLM的每一个函数,GLM会自动将矩阵相乘,返回结果是一个包括了多个变换的变换矩阵。
光栅器在三角形的三个顶点之间进行插值,并执行片段着色器遍历三角形的每一个像素。片段着色器会返回光栅器存在颜色缓存中用于显示的像素颜色值。如果片段着色器没有显式请求顶点着色器春过来的其他变量,则一般的驱动优化会丢弃顶点着色器中的无关变量。若片段着色器使用到了该变量,则光栅器会在光栅化阶段对其进行插值,并且每一次片段着色器的调用都会提供一个匹配特定位置的插值后的值。
通常境况下依赖插值的两个变量是三角形的法向量和纹理坐标。顶点的法向量通常计算包含该顶点的所有三角形法向量的平均值获得,若物体不平坦则每个三角形的三个顶点的法向量会各不相同,则可以通过插值来计算每个像素的法向量。
|
|
在各渲染管线之间进行传递的参数必须在shader中使用out保留字来全局的进行声明定义;
在图形管线中颜色值通常使用一个在[0,1]范围内的浮点数来表示;
在顶点着手其中的output输出颜色变量对应的另一边是片段着色器 的输入的颜色变量。该变量经过光栅器的插值,故每一个被执行的片段着色器可能都会是不同的颜色;
|
|
为索引缓冲创建缓冲对象的handle;
定义了坐标点及索引数组;
顶点缓冲使用GL_ARRAY_BUFFER参数表示缓冲类型;索引缓冲使用GL_ELEMENT_ARRAY_BUFFER;
除了绑定顶点缓冲还需要在绘制之前绑定索引缓冲;
索引绘制使用函数glDrawElements(),第一个参数表示待渲染的图元类型;第二个参数表示索引缓冲中用于产生图元的索引个数;第三个参数表示每个索引值的数据类型,若索引范围很小应选择小的数据类型来节省空间;最后一个参数表示从索引缓冲区起始位置到第一个需要扫描的索引值的偏移byte数,当使用同一个索引缓冲来保存多个物体的索引时使用;
|
|
glm::perspective创建了一个定义了可是空间的平截头体;第一个参数定义了视野fov的值,并设置了观察空间的大小;第二个参数设置了宽高比;第三和第四个参数设置了平截头体的近和远平面;
输入.obj文件,在解析obj文件时,若一行不是以V、VT、VN、F开头,则可将改行忽略:
区域扫描线z-buffer算法是由z-buffer算法及扫描线z-buffer算法一步步优化得来的:
1.开两个一维数组分别作为当前扫描线的z缓冲器和帧缓冲器;
2.处理当前扫描线时;
3.求出扫描线与场景中各多边形的二维投影之间的交点,一条扫描线分解为一些列区间扫描线段;
4.每一个区间扫描线段位于多边形“内部”和“外部”
5.当场景中所有多边形处理完毕时,扫描线帧缓冲器中的内容为画面在此扫描线的消隐结果;
扫描线与单个多边形内外关系
1.当扫描线与多边形的一条边相交时:
2.对每一个多边形,用一个“in/out”标记符flag跟踪记录当前扫描线段的状态:
3.对于一条扫描线上一个给定的区间,可能有多个多边形的flag值为1;
4.在当前扫描线上,跟踪flag为1的多边形
1.分类多边形表
根据多边形最大y坐标ymax将多边形放入相应类中:
2.分类边表
根据边的上端点y坐标ymax将边放入相应的类中:
3.活化多边形表
记录当前扫描线与多边形在oxy投影面上投影相交的多边形:
4.活化边表
记录投影多边形边界与扫描线相交的边对:
NOTE:
一开始想要选择从下至上扫描图像以符合glDrawPixel的行为,但是发现我们在建立分类边表时记录的是y的最大值,从而推算在其下方的扫描线与边的交点,因此还是需要从上至下扫描;所以需要进一步考虑存储图像的问题;
1.构建分类多边形表
遍历每一个三角形面片,计算其最大y值,创建分类多边形,将其放入对应其y值的分类多边形表中;
2.构建分类边表
遍历每一个三角形面片,计算它的三条边,创建分类边,将其放入边y最大值对应的分类边表中;
3.构建活化多边形表
由上至下扫描,将新加入的多边形加入到活化多边形表中;由于进入下一条扫描线,因此需要将多边形跨越的剩余扫描线数目减1。
在数据中需要考虑本来多边形横跨扫描线数目就为0的多边形,此处考虑将剩余扫描线数目小于等于0的多边形都从活化多边形表中删去;
4.构建活化边表
遍历分类边表,更新原有边表,加入新的与扫描线相交的边;
5.zbuffer计算
需要注意在程序中要保证整个图形的各个三角面片都在window中,超出window的部分没有进行考虑;
1.在这里稍微对dy进行调整,dy表示边占据的扫描线的条数,例如在同一扫描线上的边其dy为1,加入边表时,跨越的剩余扫描线数目则为0,在下一次处理时由于对resDy的更新,其小于0,将从活化边表中删除;
2.遗留问题:似乎在inPolygon中加入三角面片之后,无法合理删除,因为观察获得的颜色结果,相同颜色占据了后面一大段而没有其他的变化,应该是存在问题。
3.程序实现了,剩下需要对输入的图形进行尺寸调整;
|
|
我们使用framBufferData存储每个像素的颜色值,共需width x height x 3的存储空间。
该函数用于对齐像素字节,默认4字节对齐,即图像数据字节必须是4的整数倍,则读取数据时,读取4的整数倍的字节数据来渲染一行。若为RGB图像,则一行10个像素共30字节,在4字节对齐模式下,会读取32个字节,这将导致读取越界;因此,此处将数据读取方式改为1字节读取形式,这种方式牺牲了存取效率,但是保证了不会出现存取越界的情况;
该函数设置像素点绘制的起始位置,此处是从窗口中心对像素进行一行一行的绘制;若不适用该函数,则之后的像素将从左下角开始绘制;
根据frameBufferData中存储的颜色数据绘制每一个像素点的颜色,width和height控制绘制图像的宽和高,这两个两和窗口的宽和高没有关系;GL_RGB设置的是frameBufferData中存储的颜色值的排列顺序;GL_FLOAT表示数据的类型;
程序测试结果
OpenGL的可编程管线处理流程如下:
|
|
创建着色器程序,将所有的着色器连接到该对象上;
创建着色器对象,shaderType选择GL_VERTEX_SHADER表示顶点着色器,GL_FRAGEMENT_SHADER表示片段着色器;
编译着色器对象前必须定义它的代码源;
检查着色器相关的错误;
将编译好的shader对象绑定到程序对象上;
编译好所有的shader对象并将它们绑定到程序中,之后完成连接。在实现程序连接之后可以通过调用glDetachShader和glDeleteShader解绑并删除着色器,若着色器绑定在程序上,只调用删除函数只会标记该着色器等待删除,只有调用解绑之后才会将其置零并删除;
检查和程序相关的错误;
检查当前的管线状态程序是否可以被执行;
使用回调函数将连接好的着色器程序设置到管线声明中,该着色器程序将在所有的绘制回调函数中一直生效知道你用另一个程序替换掉它或者使用glUseProgram指令将其置为NULL明确禁用它。
告诉编译器的目标GLSL编译器版本是3.3;
在顶点着色器中,使用location指定输入变量,使得可以在CPU上配置顶点属性;使用layout将变量与顶点数据链接,这里位置变量的属性位置为0;
让编译器知道缓冲区中顶点的哪个属性和shader中声明的哪个属性进行映射匹配有两种办法:
|
|
可以通过连接多个shader来创建着色器,但是每个着色器阶段(顶点着色器,几何着色器,片段着色器)有且只有一个main函数作为着色器的入口。
这里对输入顶点的位置做固定编码转换,‘gl_Postion’是一个内置的变量,用来保存顶点位置,光栅器会查找这个变量并用在一系列变换之后将它作为屏幕空间的位置。此处需要设置W值为1.0。将物体从三维投影到二维的过程需要经过两个独立的阶段:首先,将所有顶点乘以投影变换矩阵;然后,在顶点到达光栅器之前GPU会自动对位置属性进行‘透视分割’,即使用W值分割gl_Position的所有其他分量元素。
片段着色器定义每个片段(像素)的颜色,输出的颜色通过上面代码中声明的变量完成,在这些变量上设置的值将会被光栅器接受最后写入帧缓存中;
uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,一致变量与普通属性的区别:
|
|
将渲染回调函数注册为全局闲置回调,若使用一个专用的闲置回调则需要在它后面添加一个glutPostRedisplay(),该函数标记当前的窗口需要被重新绘制,当下一个GLUT的主循环开始时就调用渲染回调函数进行窗口绘制。
我们无法直接获取着色器的内容,因而也无法改变它的变量值,当编译着色器对象时,GLSL编译器会给每一个一致变量分配一个索引,在着色器内部表示中,是通过变量的索引值来获取编译器内部的变量的,该索引可通过glGetUniformLocation函数设置程序对象的句柄参数和变量名参数来获取,若有错误则返回-1.
glUniform{1234}{if},该实例函数可以将浮点数或整型数赋给不同维度的向量作为参数;
|
|
glew.h需要放在其他OpenGL头文件之前。
|
|
调用函数初始化GLUT。
配置GLUT的选项:GLUT_DOUBLE在渲染结束后开启双缓冲机制,即维护两个图像的缓冲数据,屏幕显示一幅图像时后台同时绘制另一份图像缓冲数据,交替显示;GLUT_RGBA颜色缓冲。
创建窗口。
在窗口系统中工作时,使用事件回调函数与运行的程序进行交互,这里使用主回调完成一帧图像的所有渲染工作,该回调函数会在GLUT内部不断循环调用。
OpenGL是一个状态机,调用一个改变状态的函数后,具体的配置保持不变,直到下次再调用这个相同的函数时再次改变状态和配置。
该函数调用传递指令给GLUT开始内部循环,在循环中该函数监听窗口系统中的事件并通过配置的回调函数传递出去;在本例中,GLUT将只会调用在glutDisplayFunc中注册的回调函数RenderScenceCB渲染图像。
glClear(GL_COLOR_BUFFER_BIT)清空颜色缓存,glutSwapBuffer函数告诉GLUT交换双缓冲机制中前后两个缓存的角色位置。
顶点缓冲对象VBO,是在GPU中加载顶点最有效的方式。
GLEW库需要在放在其他OpenGL库的前面。
用于OpenGL的3d数学库。
初始化GLEW并检查是否有错。
创建Vector3f结构的数组,并初始化XYZ坐标为0,使该点位于屏幕中央。
定义一个GLuint引用变量,来操作顶点缓冲器对象。
OpenGL定义了若干glGen*前缀的函数来产生不同类型的对象,它们通常有两个参数:第一个参数用来定义想创建的对象的数量,第二个参数表示GLuint变量数组的地址,用于存储系统分配的引用变量handles。之后对该函数的的电泳不会重复产生相同的handle对象,除非先使用glDeleteBuffers删除对象。不需要在buffer中定义具体要做的事情,之后会进一步进行具体工作设定。
|
|
在OpenGL中需要将handle与一个目标的名称进行绑定,然后在该目标上执行命令,然后在该目标上执行命令。这些指令只会与handle绑定的目标上生效直到另外有其他的对象跟这个handle绑定或者这个handle被清空。此处GL_ARRAY_BUFFER为handle,将VBO绑定与该handle绑定。目标名GL_ARRAY_BUFFER表示这个buffer将存储一个顶点的数组。
绑定对象之后,需要往里添加数据,该回调函数的参数为已绑定的目标名参数GL_ARRAY_BUFFER,数据的比特数参数,顶点数组的地址,表示数据模式的标志变量。由于现在不会改变这个buffer的内容,所以使用了GL_STATIC_DRAW标志,该标志变量只是用于提示OpenGL驱动程序可以通过它来进行启发式的优化(如:内存中哪个位置最合适存储这个buffer缓冲)。
|
|
在shader着色器中,为每个顶点属性(位置、法线等)添加索引,使得C++程序中的数据和着色器中的属性名称绑定。必须开启每一个顶点的属性,否则渲染管线无法获取这些数据。
再次绑定buffer准备开始调用回调函数;在有很多buffer缓冲存储的不同模型的程序中,必须用将要调用的buffer来不断更新管线的状态。
该回调函数告诉管线如何解析buffer中的数据:第一个参数定义属性的索引;第二个参数指属性中元素的个数;第三个参数指每个元素的数据类型;第四个参数指是否希望属性在被管线使用之前被单位化;指缓冲中属性的两个实例之间的比特数距离(若有一个包含位置和法向量两个属性6个浮点数的数据结构的数组时,此值为24);最后一个参数设置属性在数据结构中的内存偏移值;
调用顺序绘制回调函数绘制几何图形,该指令将整合这个指令收到的绘制参数和之前为该店建立的状态数据来将结果渲染在屏幕上。第一个参数定义拓扑结构;第二个参数表示第一个要绘制的顶点的索引;最后一个参数是要绘制的顶点数;
OpenGL提供了不同类型的绘制回调,通常可以分为顺序绘制和索引绘制:
|
|
在顶点短时间内不使用时即时禁用顶点。
三维CAD建模是秋学期上的课,课程作业是实现边界表示Brep中的半边数据结构,并且实现欧拉操作完成物体的扫成。实现的过程中遇到了许多琐碎的问题,这里就记录一下算法以及一些遇到的小问题。
半边数据结构是C++编写的,不需要用到其他的库,但是显示图形需要借助OpenGL,我又希望添加一些功能所以是在Qt的基础上写的,但是出现了一些奇怪的bug暂时没有解决,希望之后熟练了OpenGL之后可以解决这个小bug。
|
|
运用欧拉操作,可以构建三维物体边界表示的拓扑关系。由于操作符合欧拉公式v-e+f=2(s-h)+r因而得名,主要实现的6个操作:
|
|
实现了平移扫成的过程,通过给定移动的方向向量及位移距离计算出物体的顶点。
在Java中,异常对象都是派生于Throwable类的一个实例,Throwable又分解为:Error和Exception。Exception又可分解为两个分支:
一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。如果在子类中覆盖了超类的一个方法,子类方法中声明的受查异常不能比超类方法中声明的异常更通用。如果超类方法没有抛出任何受查异常,子类也不能抛出任何受查异常。
创建异常类,只是定义一个派生于Exception的类,或者派生于Exception子类的类。
若某个异常发生的时候没有在任何地方进行捕获,那程序就会终止,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。要捕获一个异常,必须设置try/catch语句块。
当代码抛出一个异常时,就会终止方法中剩余代码的处理,并退出这个方法的执行,若方法获得了一些本地资源,并且只有这个方法自己知道,若这些资源在退出方法之前必须被回收,则就会产生资源回收问题。使用finally子句,可以保证不管是否异常被捕获,finally子句中的代码都会被执行。建议解耦合try/catch和try/finally语句:
堆栈轨迹是一个方法调用过程的列表,包含了程序执行过程中方法调用的特定位置。
ImgaeView用于在界面上展示图片,我们需要根据图片的分辨率,将它放在不同的目录下,目录的设置对应手机屏幕的密度dpi:
|
|
Android会按照手机屏幕密度匹配最适合的图片,如果在最适合密度对应的目录下没有找到,则向高密度文件夹中寻找图片,并在显示图片时缩小图片;若仍没有找到,则在低密度文件夹中寻找图片,并在显示图片时放大图片。
|
|
上面两种表达式效果相同,前者是匿名内部类的表达方式,后者是Lambda表达式。
接口,主要用来描述类具有什么功能,而并不给出每个功能的具体实现,提供实例域和方法实现的任务应该由实现接口的类来完成。函数式接口,即只有一种抽象方法的接口,需要这种接口的对象时,可以提供一个lambda表达式。
匿名内部类即只能使用一次的内部类,必须继承一个父类或实现一个接口,注意通用格式中为InterfaceType。
由于匿名类没有类名,因此匿名类没有构造器,对于在内部类实现接口的时候,可以使用lambda表达式来表达。
原始程序在AndroidManifest.xml中设置
但是会导致程序点击按钮后无法跳出对话框,并且程序闪退,这里是设置theme错误,要修改为
当出现Android no debuggable processes时,需要设置
Tools -> Android -> Enable ADB Integration active
设置完毕之后,需要重启Android Studio重新运行程序。
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true