OpenGL和GLSL入门,实现简单的纹理映射,法线映射以及简单光照明模型

本人不是计算机专业出身,本科是数学专业的,最近读在职研究生的课程,图形学的课程老师要求用着色器实现一个简单的纹理映射和法线映射,因为之前没什么编程基础,很多基本的东西都不会,因此是从零学起,先在网上下载了一个VS2017,学了一下C++,然后按照教程配置GLUT和GLEW,老师给了一个叫FreeImage的库,用来加载图片的。有了这几个工具就可以直接做了,不需要再下载glm库之类的。
网上很多教程需要各种扩展的函数库,我之所以没用一是因为配置起来比较麻烦,二是毕竟我要交作业,如果我包含很多其他的函数,老师那里配置起来也麻烦,因此最终我的代码只有一段,不包含除了上面说的几个工具之外任何一个头文件。
而且我在学习的过程中发现,网上有很多教程,因为写的时间不一样,用的标准也不相同。因为OpenGL的标准经历过很多的变化,因此我特意查了一下,到底各个标准之间是什么关系。也会写在里面。
我学了这段时间有一些体会,至少我实现的这些功能都弄明白了,因此写了这篇东西,算是对自己学习的一个总结。由于篇幅原因,我没有写得太细致,很多细节的问题看一眼源代码就可以很清楚,再不清楚的话网上查一查也就知道了,我就把一些基本思想以及我遇到的小问题写下来。
由于本人刚入门,以后有一些新的想法也会陆续写下来。

  • 1、OpenGL到底是什么,以及几个其他工具的介绍
  • 2、简单的窗口管理
  • 3、如何画一个图形
  • 4、坐标变换是怎么回事
  • 5、GLSL语言是什么,什么是着色器(shader)
  • 6、着色器的信息传递
  • 7、纹理贴图基本的思想
  • 8、光照
  • 9、法线贴图
  • 10、用鼠标实现简单交互
  • 11、着色器中一些其他问题的说明

1、OpenGL到底是什么,以及几个其他工具的介绍

我之前由于不是计算机专业,基本没接触过编程语言,只用过MATLAB。以为OpenGL就跟MATLAB一样,上网下载一个东西,进去有个窗口就能编程了。后来才明白,OpenGL是一个开放的平台,你可以用任何语言,在任何平台上实现,而且已经内置在里面,不用再下载新东西。我看到网上有人用JAVA,而我用的C++,在VS2017上面实现的,也就是说,你想学习OpenGL只需要有一个Visual Studio 2017,里面有C++就行了,除了我下面说的几个外,就不需要再下载别的东西了。
下面说说GLUT。OpenGL虽然功能强大,但并没有窗口管理界面,也就是说你弄了半天,它不能创建窗口,把你想看见的东西显示出来,这就很尴尬了,因此有一个专门做窗口管理的库,叫做GLUT。这个东西网上就可以直接下载,配置实际上就是把几个文件拷到几个文件夹中就行了。你会看到还有一个叫FREEGLUT的,功能与GLUT其实是一样的。只是GLUT貌似1998年之后就没有更新过了,而FREEGLUT还有人更新,但由于我们也不需要它来实现什么复杂的工作,只是用来作窗口管理,因此GLUT足够了。我也试过FREEGLUT,但因为要自己生成文件,总配置不成功,因此就直接用GLUT了。
GLEW是为了实现着色器的某些功能而用的,具体都有哪些功能我现在也不是太清楚,但它也是必须的。跟GLUT一样下载下来,按照网上教的配置好就行了。
而FreeImage是一个图形库,在我这里它就是一个读取图片文件的东西,因为这个库的存在,使得读取图片变得很简单。OpenGL也可以直接读取图片,但一是代码比较麻烦,二是有很多图片格式的限制,用这个库就省了很多麻烦。我后面要讲的纹理贴图法线贴图等都需要读取图片,因此跟前面一样,把它下载下来,一样的方法配置好就行了。
看过网上教程的可能会发现,有些人没有用GLUT,而是用的GLFW,这两个东西都是用来管理窗口的,功能类似,因此有些程序中某些函数看到glfw开头的不要陌生,类比glut开头的就行了。

2、简单的窗口管理

下面我们就要开始正式的编程了。
首先我们打开vs2017,新建项目->Windows桌面向导,输入个名称->选控制台应用程序,只勾选空项目,确定就可以了。
然后我们新建一个源文件(.cpp),就可以开始写了。
我们先把头文件都包含进去,注意#include<GL/glew.h>要写在#include<GL/glut.h>之前,具体我也不知道为什么,如果写在之后会有错误。
再有就是main函数就是管理窗口用的,其中:

glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA);
glutInitWindowPosition(100, 100);
glutInitWindowSize(winWidth, winHeight);
glutCreateWindow("不忘初心方得始终");

功能是初始化GLUT,并给窗口定位,定义窗口大小,并且给窗口命名的,到时候在我的源码上改一改就明白作用了。类似的我们还可以在main函数中定义窗口的背景颜色等等,具体请看我源码注释。

3、如何画一个图形

有了窗口,我们就要画一些图形,我这个程序中画了一个立方体。OpenGL和GLUT中有一些自带的函数,比如很经典的犹他茶壶glutSolidTeapot,就是用贝塞尔曲线画的,这些书本上都有,但我们事实上不需要知道具体的过程,我们只需要用这个函数就可以画出来了。自带的函数虽然简单,但有些东西,比如法线,纹理坐标等我们不方便自己设置,因此我们应该知道如何来画一个简单的图形。
首先我们要知道顶点缓冲区,我们可以这么理解,我们要画一个东西,首先要知道这个东西在空间中的位置,这个位置用什么刻画呢,就是顶点,比如立方体有8个顶点,我们把这些顶点的坐标信息输入到GPU中就可以画出来了。储存这些顶点信息的地方就叫顶点缓冲区。当然画的时候并不是直接就画出了这个立体图形,而是一个面一个面地画,还要标明我们到底要画什么图形,这个用OpenGL中一些定义好的常量表示,比如我要画四边形,就是GL_QUADS。
系统是这样操作的:比如我们要画三角形,它就会从第一个顶点开始,依次读入三个点,画一个三角形,再读入三个点,再画一个。而在实际中,我们不可能只画一个三角形,可能需要画很多三角形把他们连起来取近似一个曲面,因此这些三角形有很多共享的顶点。比如两个三角形共用一条边,事实上只有四个顶点,但我们为了画它,就必须输入六个顶点信息,让它画两个,否则就画不出来了。这样增大了我们的工作量,因此我们引入一个新的概念,就是索引缓冲。
比如我们画立方体,如果按照四边形来画,总共要画六个面,每个面4个顶点,总共要输入24个顶点信息,而事实上这24个顶点每个都有三个重复的,因为每个顶点都是三个面共用的。所以我们将立方体的8个顶点编号,每次绘制一个面就从这8个编号中选,这样我们可以定义一个包含24个编号的区域,称为索引缓冲,这24个编号是从0-7这8个数字中取的。因为每个顶点都是浮点型,空间很大,而索引可以用整型,第一是空间小,第二是我们可以减少重复的输入,也方便修改。
当然,我这个程序中,因为就画一个立方体,而且每个面的法向量不同,所以没有用索引缓冲,但这个概念需要理解。
以顶点缓冲为例,索引缓冲也类似,过程是这样实现的:我们要先创立一块顶点缓冲区,然后用一个变量绑定到这个缓冲区,还要声明它的大小:

glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

之后我们就可以用一个函数来完成绘制的过程,在这个函数中我们可以指定很多东西,比如消除隐藏面,视见体,观察坐标系选择,最重要的是画出这个图形

glDrawArrays(GL_QUADS, 0, 24);

第一个参数是指画的四边形,第二个参数是指从第几个顶点开始画,第三个参数是指总共画多少个顶点,刚才我们已经说了,每个面都要画,因此是24个顶点。
当然,顶点信息也不只是坐标,还包括纹理坐标,法向量,切向量等,我们后面要一一说明。

4、坐标变换是怎么回事

我们在前面画完了一个图形后,就要把它显示出来,那么怎么显示呢?
我们要先定义一个投影的方式,我们一般都用透视投影,这是符合正常观察规律的,就是所谓的近大远小。那么我们就要定义一个观察的方向,这个方向包括我们眼睛的位置以及看向的位置。

gluLookAt(0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.1, 0.0);

其中前三个分量表示眼睛的位置,4-6个分量表示看向的中心的方向,后三个分量表示头顶向上的方向,这个也可以自己改变数值去试一下。
下面这个函数:

glFrustum(-1.0, 1.0, -1.0, 1.0, 2.0, 8.0);

定义了一个称为视见体的东西,事实上就是一个平截头体。具体的图我就不画了,感兴趣的可以上网找,或者任意的计算机图形学的书上都有。前四个分量分别是眼睛前面那个面的上下限,后两个分量表示前面和后面距离眼睛的距离。
我们再看一下物体的变换,刚才我们通过在空间中定义物体的顶点来定义物体的大小和位置。这个物体画好后,我们可以对它进行一些变换,包括移动位置,旋转它,或者是放大/缩小它。
无论我们怎么变化它,它顶点的坐标都会发生一些变化,而这个变化如何表示就是我们主要研究的问题。事实上,无论怎样变化,都相当于是把顶点坐标乘上一个矩阵,这个矩阵具体推导我就不写了,(主要是编辑公式太麻烦~~)感兴趣也可以去查书,我想说明的就是无论怎么变换,我们都需要把坐标乘上一个相应的矩阵。
在一些教程中我们经常能看到坐标用一个四维的向量来表示,我们经常用的是三维的坐标,四维的是怎么回事呢?这就是所谓的齐次坐标系,具体也可以查书,我们只需要知道这样做的目的是为了让各种变化更方便用矩阵乘法表示就可以了。而一边前三个分量就是坐标,而第四个分量通常取1。(当然有时不是1,但这里我们暂时不涉及)
比如我程序中是将物体旋转:

glRotatef(angley, 0.0, 1.0, 0.0);
glRotatef(anglex, 1.0, 0.0, 0.0);

当然,前面说的投影也可以用矩阵表示出来,这个在后面还会用到。

5、GLSL语言是什么,什么是着色器(shader)

下面说到核心的阶段了,在讲具体内容之前,我们要先知道图形是怎么在显示器上显示出来的。
刚才我们通过程序画了一个图形,并设定好了我们如何观察它,但它究竟是什么颜色,我们并不知道。而我们现在应该知道,显示器唯一能显示出的就是颜色,不管多复杂的图形,最终显示器都是通过一个一个的像素的颜色把物体显示出来的。而颜色就是我们通常知道的红绿蓝(RGB)三种颜色的叠加。所以每一种颜色都是三个分量,用一个向量来表示,每个分量的范围都是0-1,这一点很重要,我们后面还会用到。一般的,我们还会将颜色中再加入一个分量A,表示透明度,比如我们在一个玻璃中看到外面的景色,就是透明玻璃的颜色和外面东西的叠加,现在我们还涉及不到,因此一般也将这个分量设置为1,即不透明。
计算机会先对图形的顶点进行各种变换,然后投影,这些其实都是做矩阵的乘法,最后得到最终顶点的坐标,然后开始光栅化,简单地说就是把这个图形分解成一个一个的小片,让显示器的像素把它显示出来。之后会再计算每一个小片的颜色,把这些传递到缓存中,最后打到显示器上。
这个过程中尤其重要的就是处理顶点坐标的环节和计算像素颜色的环节,我们把这两个过程叫做着色器(shader)。虽然我们现在不知道着色器具体是怎么操作的,但我们至少应该了解,着色器顾名思义就是通过各种计算让显示器知道窗口中每个像素是什么颜色,不管是纹理贴图还是光照什么的,都是通过着色器来实现的。
在过去,因为硬件的局限性,能显示的东西也有限,因此很多着色器都是编好的,我们只要像调用函数那样调用它就行了,实现的功能很单一。后来随着硬件的发展,我们已经不满足于这些固定功能的着色器,这就需要自己编写一些功能,在OpenGL中编写着色器的语言就称为GLSL语言。
在一些教程中我们会看到固定管线和可编程管线的说法,就是指的着色器是固定功能的还是可编程的,我们的显卡现在一般也都支持可编程的管线。我这个作业的重点也是着色器的编写,即GLSL语言的使用。
下面我们来看一下如何使用可编程着色器及GLSL语言。
我们不能像编译程序那样直接在VS中编写,因为着色器通过一种独特的方式被读入程序,因此我当时看的时候是一头雾水。后来明白了,简单地说,我们要先在记事本中用GLSL语言编写着色器程序,然后在我们的程序中读取出记事本中的字符串,然后用一些命令去编译。这些放着色器程序的文本文件要放在工程的文件夹下。(也就是你放源代码.cpp文件的那个文件夹)读取字符串的函数是这样的:

char *readTextFile(const char *name)
{
    FILE *fp;
    char *content = NULL;
    int count = 0;
    if (name == NULL)
        return NULL;
    //fopen函数有可能提示要求用fopen_s
    fp = fopen(name, "rt");
    if (fp == NULL)
        return NULL;
    fseek(fp, 0, SEEK_END);
    count = ftell(fp);
    rewind(fp);
    if (count > 0)
    {
        content = (char*)malloc(sizeof(char)*(count + 1));
        if (count != NULL)
        {
            count = fread(content, sizeof(char), count, fp);
            content[count] = '\0';
        }
    }
    fclose(fp);
    return content;
}

而在程序中编译着色器的函数是这样的:

void setShaders(void)
{
    //创建着色器对象
    GLuint vertShader, fragShader;
    vertShader = glCreateShader(GL_VERTEX_SHADER);
    fragShader = glCreateShader(GL_FRAGMENT_SHADER);
    GLchar *vertSource, *fragSource;
    //读入着色器程序字符串
    vertSource = readTextFile("vertexshader.vert");
    fragSource = readTextFile("fragmentshader.frag");
    
  • 5
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值