1 引子

虽然是计算机科班出身,但从小对几何方面的东西就不太感冒,空间想象能力也较差,所以从本科到研究生,基本没接触过《计算机图形学》。为什么说基本没学过呢?因为好奇(尤其是惊叹于三维游戏的逼真,如魔兽世界、极品飞车),在研究生阶段还专门选修计算机图形学,但也只是听了几堂课,知道了有帧缓存、齐次坐标等零零散散的概念,之后读了一篇论文并上台作报告(压根没读懂)。总之,当时只是觉得计算机图形学或三维渲染很牛,甚至问我什么是渲染都不知道,更不知道如何将3维几何体显示到2维屏幕上。令我现在想来非常可笑的是,当时以为2D图像才是平面的,3D图像就是立体的。

真正接触3维绘制方面的知识是在工作后,因为是搞图像处理与可视化的方面软件的开发,所以开始知道了这些3D图像是通过OpenGL/Direct3D等编程接口来做的。不过公司关于OpenGL接口的调用和绘制方面的代码是另一个组写的,博主基本看不到他们的代码。不过,与相关同事的讨论中还是学到了一些知识。于是,博主便考虑系统地学习一下OpenGL编程。

市面上最好的两本OpenGL的书应该是——《OpenGL编程指南》(红宝书)和《OpenGL编程宝典》(蓝宝书)。于是,就挑选了《OpenGL编程指南(第八版)》作为我的启蒙教材,由此踏上学习之路。本博客希望记录学习过程中的一些新的体会。记下来的知识才是自己的。闲话不多说,让我们踏上OpenGL学习之旅吧!

2 第一个例子

红宝书一开始在什么是OpenGL一节,简要介绍了OpenGL的概念、发展历程、渲染流程等概念。说实话,一开始读这段文字压根就是认字,概念各种不懂。不过没关系,博主觉得,大部分技术书籍的第一章都是这样——罗列一大堆概念,然后写一个简单的小程序,对它分析分析,继续列举更多概念,然后说明将在第几章第几节详细介绍。所以对于我这种初学者,看完第一章完全不知道说什么是也很正常,别急,慢慢的,某一刻你就理解了——所谓一通百通。

之后,咣当扔过来一个例子,这也许就是程序员的风格——先写个Demo看看。其实,刚开始对这个例子比较藐视,太简单了——和我脑海中高大上的游戏界面相去甚远。不过,其实这个例子中还是包含很多东西的。首先来看看,这个程序的运行效果。

就是在一个窗口中绘制两个蓝色的三角形。下面我们就按步骤来绘制这个图形。

第一步:搭一个框架

对于技术书籍,一开始看书就是照着书本敲代码,敲完代码再看代码有没有问题。这个例子虽然简单,但是代码不少。其实我们可以一步一步来写,首先写一个main函数,在里面填一些初始化和创建窗口的代码,如下:

 
 1 #include <iostream>
2 #include "StdAfx.h"
3
4 void display()
5 {
6 }
7
8 int main(int argc, char **argv)
9 {
10 glutInit(&argc, argv);
11 glutInitDisplayMode(GLUT_RGBA);
12 glutInitWindowSize(512, 512);
13 glutInitContextVersion(3, 3);
14 glutInitContextProfile(GLUT_CORE_PROFILE);
15 glutCreateWindow(argv[0]);
16
17 if (glewInit())
18 {
19 std::cerr << "Unable to initialize GLEW... Exiting..." << std::endl;
20 std::exit(EXIT_FAILURE);
21 }
22
23 glutDisplayFunc(display);
24 glutMainLoop();
25 }
 

其实这里只是涉及到OpenGL的API,只是用到了第三方库一些函数创建了一个显示图像的窗口。就像我们开始写控制台Demo的时候,先写main函数,然后打印一个“HelloWorld”出来,看看能不能跑。上面这短短的几行代码是能够运行的。运行结果就是一个大小为512×512的空白窗口。代码很简单,就是初始化相关的函数。这里需要说明一下的是:#include "StdAfx.h"是从红宝书的网站上下载下来vgl.h文件,并配置了工程的属性(如头文件目录、lib库目录);另外一个就是:display函数就是我们要调用OpenGL绘制图像的函数。下面就是填充这个函数。

第二步:填充框架

和任何程序一样,OpenGL程序需要输入,然后经过渲染管线,即一系列的着色器(着色器贯穿本书的始终),最后得到一个二维图像(像素矩阵),见下图。

所以在调用OpenGL API进行绘制图像之前,先将所需数据加载到显存中,以便于OpenGL在绘制时对其进行相关处理。填充后的代码如下:

 
 1 #include <iostream>
2 #include "StdAfx.h"
3
4 GLuint Buffer_ID;
5 const int BUFFER_NUMBER = 1;
6
7 GLuint VAO_ID;
8 GLuint VAO_NUMBER = 1;
9
10 const int VERTICES_NUMBER = 6;
11 const int vPosition = 0;
12
13 void Initialize()
14 {
15 //---------------------准备数据-------------------------------
16 GLfloat vertices[VERTICES_NUMBER][2] =
17 {
18 { -0.90, -0.90 },
19 { 0.85, -0.90 },
20 { -0.90, 0.85 },
21
22 { 0.90, -0.85 },
23 { 0.90, 0.90 },
24 { -0.85, 0.90 }
25 };
26
27 // 生成缓存对象
28 glGenBuffers(BUFFER_NUMBER, &Buffer_ID);
29
30 // 绑定缓存对象
31 glBindBuffer(GL_ARRAY_BUFFER, Buffer_ID);
32
33 // 填入数据
34 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
35
36 //-------------------设置顶点数据属性------------------------------
37 // 生成顶点数组对象
38 glGenVertexArrays(VAO_NUMBER, &VAO_ID);
39
40 // 绑定顶点数组对象
41 glBindVertexArray(VAO_ID);
42
43 // 设置顶点属性
44 glVertexAttribPointer(vPosition, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
45 glEnableVertexAttribArray(vPosition);
46 }
47
48 void display()
49 {
50 glClear(GL_COLOR_BUFFER_BIT);
51
52 glBindVertexArray(VAO_ID);
53 glDrawArrays(GL_TRIANGLES, 0, VERTICES_NUMBER);
54
55 glFlush();
56 }
57
58 int main(int argc, char **argv)
59 {
60 glutInit(&argc, argv);
61 glutInitDisplayMode(GLUT_RGBA);
62 glutInitWindowSize(512, 512);
63 glutInitContextVersion(3, 3);
64 glutInitContextProfile(GLUT_CORE_PROFILE);
65 glutCreateWindow(argv[0]);
66
67 glewExperimental = TRUE;
68 if (glewInit())
69 {
70 std::cerr << "Unable to initialize GLEW... Exiting..." << std::endl;
71 std::exit(EXIT_FAILURE);
72 }
73
74 Initialize();
75 glutDisplayFunc(display);
76 glutMainLoop();
77 }
 

在原有基础上,添加了加载数据部分和绘制图形部分的代码。主要分了两个步骤:

1. 数据输入步骤

任何系统都有输入输出(I/O)系统,如计算机硬件系统中有输入设备和输出设备;每一个编程语言都有自己的输入命令(类)和输出命令(类);对于一个算法来说,也有其输入和输出。

对于我们图形绘制系统来说,自然也少不了输入和输出。由于数据的输入只需要执行一次就可以,故写在Initialize函数中,并在main函数是执行。

本例要绘制两个三角形,输入的数据自然就是两个三角形的顶点数据。由于绘制的是平面三角形,我们可以不指定z方向的坐标值(深度值)。16~25行的二维顶点数组是存放在内存中的,图形绘制是在显卡中执行的,所以需要将这些数据加载到显存中。这里出现了OpenGL编程中第一个重要的概念——缓存对象(Buffer Object)。顾名思义,这一对象主要就是用来存放数据的,在这里,我们使用缓存来存放顶点数据。下面,我们来看看程序中是怎么使用缓存对象来加载顶点数据的。

加载顶点数据到显存用了3条OpenGL API来实现数据的加载。

I:使用glGenBuffer声明一个缓存对象ID。编程语言中通过变量的方式来标识内存中的数据;操作系统中通过各种ID来感知各个实体,如进程标识符PID来标识进程,线程标识符TID来标识线程。OpenGL也是通过ID来标识各种对象。由于这里只要使用一个缓存对象,所以只要生成一个缓存ID即可,但要注意,这条指令可以生成多个缓存对象

II:使用glBindBuffer来绑定其中一个缓存。刚才已经提到,缓存对象可以有多个,那OpenGL怎么知道要当前操作的是哪个缓存对象呢?这就需要使用glBindBuffer命令——这个命令的作用就是激活(Activate)其中一个缓存对象。参数很简单,就是刚才生成的缓存ID。

III:使用glBufferData来分配内存并拷贝数据到显存。这一步是我们最终目的——将数据从内存拷贝至显存。这个函数和C语言中内存拷贝memcpy很类似,函数签名为:

void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);

target  ——刚才绑定(激活)的缓存对象,这可以看做memcpy的目的地址;

size   ——这就是数据的大小,这和memcpy中的数据大小是一样的;

data    ——源数据的指针,这和memcpy中的源数据指针是一样的;

usage  ——这个参数指定这个数据的用法,主要是为了优化OpenGL的内存管理——根据使用方法确定最优显存分配方案。

通过使用上述三条OpenGL API,我们就完成了数据从内存加载到缓存的功能。到此为止,故事还没有结束,OpenGL在获取顶点数据时并不知道缓存对象中的数据如何解析,所以需要告诉OpenGL,刚才上传的数据的格式是怎么样的。这就引入了第二个对象——顶点数组对象及顶点属性的概念。顶点数组对象就是用来描述刚才上传的顶点数据特征的一个对象,下面就继续来分析与顶点数组对象的相关API。

I:使用glGenVertexArray声明一个顶点数组对象ID。这和缓存对象ID是一样的,都是为了便于OpenGL的组织管理;

II:使用glBindVertexArray来激活其中的一个顶点数组对象,和缓存对象也是类似的;

III:使用glVertexAttribPointer接口来填充当前绑定的顶点数组对象。这个函数的功能和缓存对象的glBindBuffer命令是一样的,只是对于缓存对象来说,只要拷贝一下数据就可以了,而这里需要填充顶点属性数据(就像填充一个结构体一样)。这个函数的参数比较多,其函数签名为:

void glVertexAttribPointer(GLuint index, GLsize size, GLint size, GLenum type, GLboolean normalized, GLsizei stribe, const GLvoid *pointer);

index      ——这是指定在该顶点在着色器中的属性。

size         ——该参数指定了每个顶点有几个分量,本例中二维顶点,故设为2;

type        ——该参数指定了顶点中分量的数据类型,这里顶点的坐标分量是浮点型数据,故设为GL_FLOAT;

normalized  ——该参数表示顶点存储前是否需要进行归一化;

stride      ——该参数指定两个顶点数据之间间隔的字节数,在本例中,顶点是连续存储的,故设为0;

pointer    ——顶点数据在缓存对象中起始地址,在本例中,因为缓存对象中只存放了一个顶点数组,所以这一值设为0。

IV: 使用glEnableVertexAttribArray来启用与index索引相关联的顶点数组。虽然前面设置了顶点数组属性,但如果没有启用的话,数据依然无法被OpenGL拿到。

2. 图形绘制步骤

数据及其格式设置后之后,就是根据这一数据进行图形的绘制。这部分代码是写在display函数中的,这一函数可能会调用多次。在这个显示函数中,最重要的一个OpenGL API就是glDrawArray函数——绘制基本图形,其函数签名如下:

void glDrawArray(GLenum mode, GLint first, GLsizei count);

mode  ——指定你要绘制的图元类型,比如三角形是GL_TRIANGLES,直线就是GL_LINES,闭合的直线就是GL_LINE_LOOP,顶点就是GL_POINTS。本例中要绘制三角形,故设为GL_TRIANGLES。

first   ——指定绘制图形时的起始顶点,本例中从第0个顶点开始;

count  ——要绘制的顶点数,本例中设置为6。

给这个函数设置不同的值,将出现不同的效果——可以使用不同的顶点来绘制不同的图形。

剩下的,三个接口:

glClear(GLbitfield mask);

清空指定的缓存数据。每一次新的绘制,当然需要将上一次绘制过程中产生的一些数据给清空,以防止其对后一次绘制产生影响。在OpenGL中有三种缓存数据,分别是颜色缓存,深度缓存和模板缓存。其中深度缓存只有在三维的情形中才用到。本例中清空了颜色缓存。

glFlush();

这个接口是一个同步接口——等待绘制完成再往下执行。这里需要说明的是,OpenGL采用的是客户机-服务器模式运作的——我们的应用程序就是客户机,显卡就是服务器。每一次执行OpenGL API相当于给显卡发送一条命令,一般情况下,这些命令是以异步的方式执行的。如果我们应用程序需要等显卡命令执行完毕才能往下执行,就需要调用这个函数。

最后一个,glBindVertexArray——绑定操作对象,即glDrawArray绘制的是当前绑定的顶点数组。在本例中(只限本例)是可以不调用的,因为在Initialize函数中已经调用过了,并且display函数中没有其他的绑定。

至此,我们运行程序,应该能够看到绘制出来的是两个白色的三角形。

第三步:添加着色器

我们先来看看OpenGL中的绘制管线,如下图所示:

所谓绘制管线,就是OpenGL在绘制图像过程中所经过的操作步骤,主要有:求值器、逐顶点操作、图元装配、纹理贴图、光栅化、片元操作等等。这样的绘制管线称为固定绘制管线,因为一旦绘制开始,绘制过程人为无法干预。

着色器的引入,将绘制管线从固定的管线变为可编程的绘制管线。所谓可编程,是指在绘制固定绘制管线的过程上,可以加入我们的逻辑。什么意思呢?相当于OpenGL提供给我们一个编程框架(而不仅仅是一套API),我们可以定制其中的某些部分。这样,可以更灵活的控制绘制管线,实现更好的绘制效果。这样就得到了下面的绘制管线:

可以看出,在固定管线的基础上,增加了顶点着色器、几何着色器、片元着色器,其中顶点着色器和片元着色器最重要。顶点着色器是对输入顶点进行处理的,如对顶点进行三维变换,添加颜色等;片元着色器则是对光栅化后的片元进行处理,如纹理贴图、执行光照计算等等,也就是计算渲染颜色的(我想这也是着色器最根本的含义吧!)。这些着色器使用GLSL语言写的,这个语言语法和C语言很类似,并定义了一些内置变量和便于我们处理的接口API。

为了将上述白色三角形变为蓝色,我们可以为其添加一个片元着色器,着色器输出的就是片元的颜色。代码很简单,就是输出一个颜色值,如下:

 
1 #version 330 core
2
3 out vec4 fColor;
4
5 void main()
6 {
7 fColor = vec4(0.0, 0.0, 1.0, 1.0);
8 }
 

很简单,第1行是GLSL的版本信息。第3行表明该着色器有一个输出——计算后的颜色,在main函数中,对该输出变量赋值一个4维颜色向量,R通道值为0.0,G通道值为0.0,B通道值为1.0,A通道值为1.0(透明度),所以最后颜色就是蓝色的。

写完这个着色器程序,显卡并不知道这个着色器的存在,因此还需要一些步骤——对着色器的编译、链接并加载到显卡中。这部分内容蛮多的,我们直接使用本书提供的代码,LoadShader函数来加载着色器程序,就是在Initialize函数的最后加入下面这段代码:

 
1 ShaderInfo shaders[] = {
2 { GL_FRAGMENT_SHADER, "triangles.frag" },
3 { GL_NONE, NULL}
4 };
5 GLuint program = LoadShaders(shaders);
6 glUseProgram(program);
 

最后,书上还给出了一个顶点着色器,其实这个顶点着色器可以不用的,就是将输入的顶点坐标设置给内置变量gl_Position,有和没有效果都是一样的,为了完整性还是把它贴上来吧!

 
1 #version 330 core
2
3 layout(location = 0) in vec4 vPosition;
4
5 void main()
6 {
7 gl_Position = vPosition;
8 }
 

当然,也要把它加载到显卡中。至此,一个简单的OpenGL程序就写完了,其实修改一下顶点数据或者修改一下着色器程序,我们可以画出其他一些效果出来。比如可以画一个五角星出来,或者画一个彩色的图形出来。

3 总结

最后,总结一下:主要学习了缓存对象和顶点数组对象的创建,向缓存对象拷贝数组数据,设置顶点数组对象属性的相关接口。最后了解了OpenGL渲染管线——固定渲染管线和可编程渲染管线,在我们的程序中加入了片元着色器和集合着色器。

标签: OpenGL

OpenGL学习之路(一)的更多相关文章

  1. OpenGL学习之路(四)

    1 引子 上次读书笔记主要是学习了应用三维坐标变换矩阵对二维的图形进行变换,并附带介绍了GLSL语言的编译.链接相关的知识,之后介绍了GLSL中变量的修饰符,着重介绍了uniform修饰符,来向着色器 ...

  2. OpenGL学习之路(五)

    1 引子 不知不觉我们已经进入到读书笔记(五)了,我们先对前四次读书笔记做一个总结.前四次读书笔记主要是学习了如何使用OpenGL来绘制几何图形(包括二维几何体和三维几何体),并学习了平移.旋转.缩放 ...

  3. OpenGL学习之路(三)

    1 引子 这些天公司一次次的软件发布节点忙的博主不可开交,另外还有其它的一些事也占用了很多时间.现在坐在电脑前,在很安静的环境下,与大家分享自己的OpenGL学习笔记和理解心得,感到格外舒服.这让我回 ...

  4. OPENGL学习之路(0)--安装

    此次实验目的: 安装并且配置环境. 1 下载 https://www.opengl.org/ https://www.opengl.org/wiki/Getting_Started#Downloadi ...

  5. OpenGL学习之路(二)

    1 引子 在上一篇读书笔记中,我们对书本中给出的例子进行详细的分析.首先是搭出一个框架:然后填充初始化函数,在初始化函数中向OpenGL提供顶点信息(缓冲区对象)和顶点属性信息(顶点数组对象),并启用 ...

  6. Android学习之路——简易版微信为例(一)

    这是“Android学习之路”系列文章的开篇,可能会让大家有些失望——这篇文章中我们不介绍简易版微信的实现(不过不是标题党哦,我会在后续博文中一步步实现这个应用程序的).这里主要是和广大园友们聊聊一个 ...

  7. Android开发学习之路--Android系统架构初探

    环境搭建好了,最简单的app也运行过了,那么app到底是怎么运行在手机上的,手机又到底怎么能运行这些应用,一堆的电子元器件最后可以运行这么美妙的界面,在此还是需要好好研究研究.这里从芯片及硬件模块-& ...

  8. Qt 学习之路 2(30):Graphics View Framework

    Qt 学习之路 2(30):Graphics View Framework 豆子 2012年12月11日 Qt 学习之路 2 27条评论 Graphics View 提供了一种接口,用于管理大量自定义 ...

  9. Qt 学习之路 2(29):绘制设备

    Qt 学习之路 2(29):绘制设备 豆子 2012年12月3日 Qt 学习之路 2 28条评论 绘图设备是继承QPainterDevice的类.QPaintDevice就是能够进行绘制的类,也就是说 ...

随机推荐

  1. Android图片缩放方法

    安卓开发中应用到图片的处理时候,我们通常会怎么缩放操作呢,来看下面的两种做法: 方法1:按固定比例进行缩放 在开发一些软件,如新闻客户端,很多时候要显示图片的缩略图,由于手机屏幕限制,一般情况下,我们 ...

  2. HTML CSS——margin与padding的初学

    下文引自HTML CSS——margin和padding的学习,作者fengyv,不过加入了一些个人的看法. 你在学习margin和padding的时候是不是懵了,——什么他娘的内边距,什么他娘的外边 ...

  3. 由枚举模块到ring0内存结构 (分析NtQueryVirtualMemory)

    是由获得进程模块而引发的一系列的问题,首先,在ring3层下枚举进程模块有ToolHelp,Psapi,还可以通过在ntdll中获得ZwQuerySystemInformation的函数地址来枚举,其 ...

  4. badboy 之 查看回放结果

    在运行脚本时,Badboy提供了Summary功能方便我们监控回放结果状态,如下Summary view: 以下表格对运行情况的各个维度进行解释: 统计点 描述 Played 运行或回放脚本的次数 S ...

  5. iOS开发--基于AFNetWorking3.0的图片缓存分析

    图片在APP中占有重要的角色,对图片做好缓存是重要的一项工作.[TOC] 理论 不喜欢理论的可以直接跳到下面的Demo实践部分 缓存介绍 缓存按照保存位置可以分为两类:内存缓存.硬盘缓存(FMDB.C ...

  6. 设计数据结构O1 insert delete和getRandom

    设计一个数据结构满足O(1)的insert, delete和getRandom.这个是从地里Amazon的面经中看到的. 我们可以使用一个resizable数组arr以及一个HashMap来完成. i ...

  7. RHEL7.2下netcat工具安装教程

    1.下载 下载地址:http://sourceforge.net/projects/netcat/files/netcat/0.7.1/(下载的是netcat-0.7.1.tar.gz版本) 2.解压 ...

  8. chrome开发配置(三)安装开发工具

    1.安装 VisualStudio2010,设置环境变量 GYP_MSVS_VERSION=2010 2.安装 VisualStudio2010 SP1 3.安装 windows 8.0 sdk(不要 ...

  9. 京东商城发现了一枚Bug

    我在京东上买了几本书,发现了一个BUG.. 买书的时候,我选了京东自营的书和京东其他店的书,合在一起购买,填写了开具发票. 然后,京东处理流程是,将上面一笔订单拆分成两笔,然后发票信息没有转到其他店那 ...

  10. mtk android lcm调试

    参考MTK 文档LCM_Customer_document_MT6575.pdf The following shows the steps to add a new LCM driver: (1)  ...