因为做虚拟现实,图形学的基本知识和OpenGL的编程都要熟悉。图形学这些理论的还好,但编程的具体问题就比较麻烦了。最近半个月都在看OpenGL相关的东西,买了第8版红宝书《OpenGL编程指南》,看了前两张,熟悉了Buffer的用法,以及OpenGL到Shader中的参数传递。其实还没怎么看熟,主要是用的少。
在网上看了一个UCB的图形学导论的学堂公开课,一共0-3四份作业,前两份都还好做些。第三份作业就有些麻烦了,不像作业0截一下图,或者作业1有了理论基础后填补空缺代码就可以;作业3涉及到了具体的实现细节,所以最近看书,在网上找资料,总算在VS2010上搭起了一个最最基本的OpenGL运行框架。
其中一共用到了OpenGL,GLUT和、Glew和GLM这四个与OpenGL有关的库和一个FreeImage的库(FreeImage仅用来截图)
使用OpenGL之前建议升级显卡驱动到最新版,这样使用的OpenGL版本就是最新的。
配置的时候可以参考这个链接OpenGL基本配置。
OpenGL是核心库,可以做一些图形运算和绘制操作。因为OpenGL是跨平台的,所以没有固定的输入输出操作,不能和任一种GDI扯上关系。所以就有了下面的GLUT库。
GLUT相当于一个跨平台的虚拟层,作为OpenGL和程序员之间的接口,为程序员提供输入输出的环境,配置各种各样的回调函数,使程序员用简单的几行/十几行/几十行代码就可以实现键盘、鼠标响应和窗口重绘等操作。
GLM库主要实现了一些向量和矩阵的类和他们之间的运算,使我们使用起来更加方便。
GLEW库是为了OpenGL拓展用的。简单来说,GLEW可以提供最新的OpenGL版本中的函数,让你的机器即使没有或不能安装OpenGL的最新驱动,仍然可以使用最新版本的OpenGL的函数。但仅是提供这样的借口和软件实现,性能上当然会比真正的OpenGL核心函数要差一些。
其实还有glu库,是OpenGL的一个实用库,它的功能主要是封装了一些OpenGL中常用的函数,让程序员调用起来更方便。比如gluLookAt()函数,如果用OpenGL的核心库(就是以gl开头的函数)的话,要写好几行代码来定义ModelViewMatrix,而经过glu封装之后,只要一个gluLookAt函数就够了。
但我没有用glu库,因为最开始学习时,希望了解OpenGL更多细节上的东西。
在VS2010中配置好之后,就可以建立第一个OpenGL程序了。
然而... 老式的OpenGL程序比较直白,画点,画圆什么的,基本上看着函数名就明白是怎么回事。但现在的OpenGL为了提高效率和程序员的自主性,代码的风格和以前很不一样。所以第一次接触OpenGL的程序还是很头疼的。
像我看红宝书的第一个程序,在平面上画一个三角形,都用了将近200行代码(100行是书上列出来的主要绘制代码,还有100行LoadShader的相关函数)
然而,这段可以略过了,因为我现在已经可以写出一个新式OpenGL的基本框架了。
记录一下我建立这个框架的足迹吧。毕竟写了500行代码只画出了一个茶壶 =,= 请允许我做一个悲伤的表情。
代码很多,一次写不完。
在VS2010的工程中,自己添加的代码文件有:
variables.h // 进行全局变量的定义及声明
functions.h // 进行全局函数的定义及声明
Transform.h // Transform类声明
main.cpp // 主函数
init.cpp // 初始化的函数
shader.cpp // 加载shader的函数
display.cpp // 显示函数
keyboard.cpp // 键盘响应
mouse.cpp // 鼠标响应
screenshot.cpp // 截屏
Transform.cpp // Transform类实现
vertex.glsl // 顶点着色器
fragment.glsl // 片元着色器
先贴出两个头文件,variables.h和functions.h,这两个文件主要存放了全局的变量和函数,以供不同代码文件共享。
variables.h:
#ifndef VARIABLES_H
#define VARIABLES_H
#include <glm/glm.hpp>
// 对不需要初始化的变量,用EXTERN修饰
// 使其具有“主函数文件中定义,其余文件中声明”的作用
#ifdef MAINPROGRAM
#define EXTERN
#else
#define EXTERN extern
#endif
// 对需要初始化的变量,在主函数文件中定义,其余文件中声明
#ifdef MAINPROGRAM
extern int MainWindow = 0;
extern int WindowWidth = 450;
extern int WindowHeight = 450;
extern GLint EnableLighting = 1;
#else
extern int MainWindow;
extern int WindowWidth;
extern int WindowHeight;
extern GLint EnableLighting;
#endif
// 常量正常定义即可
const int WindowPosX = 0;
const int WindowPosY = 0;
const float DegreeGapInit = 5.0f;
const glm::vec3 Center(0.0f, 0.0f, 0.0f);
const glm::vec3 EyeInit(0.0f, 0.0f,5.0f);
const glm::vec3 UpInit(0.0f, 1.0f, 0.0f);
EXTERN glm::vec3 Eye;
EXTERN glm::vec3 Up;
EXTERN float DegreeGap;
EXTERN GLint EnableLightingLoc;
EXTERN GLint ModelViewMatrixLoc;
EXTERN GLint PorjectionMatrixLoc;
EXTERN GLint Light0PositionLoc;
EXTERN GLint Light1PositionLoc;
EXTERN GLint Light0ColorLoc;
EXTERN GLint Light1ColorLoc;
EXTERN GLint AmbientLoc;
EXTERN GLint DiffuseLoc;
EXTERN GLint SpecularLoc;
EXTERN GLint ShininessLoc;
#endif
对const变量,正常定义在variables.h中并初始化即可,这样在每个包含variables.h的文件中都有同名但独立的const变量。
对暂不需要初始化的全局变量,需要在一个文件中定义,并在其他所有使用它的文件中声明。这里采用了一个宏定义EXTERN,当在main.cpp中定义了MAINPROGRAM时,EXTERN便定义为空。而在其他文件中,没有定义MAINPROGRAM,EXTERN便被替换为extern,。此时相当于在main.cpp中进行了定义,而其他包含variables.h的文件中进行了声明。
对需要初始化的全局变量,用预处理#ifdef直接分两种情况(MAINPROGRAM和非MAINPROGRAM)进行声明/定义并初始化。
functions.h:
#ifndef FUNCTIONS_H
#define FUNCTIONS_H
#include <iostream>
using std::string;
void init();
void display();
void idle();
void keyboard(unsigned char key, int x, int y);
void specialKeyboard(int key, int x, int y);
void mouse(int button, int state, int x, int y);
void mouseMove(int x, int y);
void quitProgram(int quitStatus);
GLuint initShader(const GLenum shaderType, const char *shaderPath);
GLuint initProgram(GLuint vertexShader, GLuint fragmentShader);
void saveScreenShot(string fname);
void quitProgram();
#endif
这里就没什么好说的了。函数声明。
main.cpp:
#include <iostream>
#include <string>
using std::cout;
using std::cerr;
using std::endl;
using std::string;
#include <GL/glew.h>
#include <GL/glut.h>
#include <FreeImage.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#define MAINPROGRAM
#include "Transform.h"
#include "variables.h"
#include "functions.h"
#undef MAINPROGRAM
void reshape(int w, int h)
{
WindowWidth = w;
WindowHeight = h;
// set render area
glViewport(0,0,w,h);
// set projection matrix
glMatrixMode(GL_PROJECTION);
glm::mat4 projectionMatrix = Transform::perspective(90.0f, (float)w / (float)h, 0.1f, 99.0f);
glLoadMatrixf(&projectionMatrix[0][0]);
}
void quitProgram(int quitStatus)
{
glutDestroyWindow(MainWindow);
FreeImage_DeInitialise();
exit(quitStatus);
}
int main(int argc, char *argv[])
{
// 初始化FreeImage
FreeImage_Initialise();
// 初始化glut
glutInit(&argc, argv);
// 设置glut显示方式
glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
// 创建窗口
string windowName = argv[0];
windowName = windowName.substr(windowName.find_last_of("/\\") + 1, windowName.length() - windowName.find_last_of("/\\") - 1);
// 窗口创建方式1
{
glutInitWindowPosition(WindowPosX, WindowPosY);
glutInitWindowSize(WindowWidth, WindowHeight);
MainWindow = glutCreateWindow(windowName.c_str());
}
// 窗口创建方式2
/*{
MainWindow = glutCreateWindow(windowName.c_str());
glutPositionWindow(WindowPosX, WindowPosY);
glutReshapeWindow(WindowWidth, WindowHeight);
}*/
// 初始化Glew
GLenum err = glewInit();
if(GLEW_OK != err)
{
cerr << "Error: " << glewGetString(err) << endl;
}
// 绑定显示函数
glutDisplayFunc(display);
// 绑定空闲函数
glutIdleFunc(idle);
// 绑定键盘响应函数
glutKeyboardFunc(keyboard);
// 绑定键盘Special响应函数
glutSpecialFunc(specialKeyboard);
// 绑定鼠标响应函数
glutMouseFunc(mouse);
// 绑定鼠标移动函数
glutMotionFunc(mouseMove);
// 绑定Reshape函数
glutReshapeFunc(reshape);
// 初始化
init();
// 开始主循环
glutMainLoop();
// never come here
glutDestroyWindow(MainWindow);
FreeImage_DeInitialise();
return 0;
}
可以看到,main.cpp主要由main函数组成。其余的还有reshape()和quitProgram()这两个函数,因为这两个函数体量小,而且和程序的主要控制流也相关,所以保留在main.cpp中。
先讲main函数。 main函数主要干了三件事。第一,初始化FreeImage库、Glut库和Glew库;第二,初始化并创建一个窗口用来显示图像;第三,绑定各种回调函数用来处理键盘、鼠标等的事件响应。
在做完这些初始化动作之后,main函数调用glutMainLoop(),开始了无限的循环。并在这样的循环中显示图片与响应事件。因为是无限循环,所以glutMainLoop()函数后面的两行是永远不会到达的。写出来只是表达在形式上进行了资源释放。
简单说一下这几个回调函数。
键盘鼠标的就不说了,一看就明白,强调一下glutSpecialFunc(),用来绑定特殊按键的回调函数,特殊按键就是指小键盘的上下左右、F1-F12,INSERT等这样的功能键。glutDisplayFunc()函数是用来显示图片的,是最重要的函数;glutIdleFunc()用来绑定当CPU空闲时的工作函数;glutReshapeFunc()用来绑定当用户改变了窗口的大小时的响应函数。
quitProgram是我自己定义的一个函数,当按ESC键的时候,会激活keyboard函数,此时调用quitProgram,释放资源,退出程序。
reshape函数体现在还不能讲,大致的意思就是,使窗口中显示的内容要怎样随窗体大小变化而跟随变化。就比方说你拉长了窗口,那么里面的物体是也相应地拉长呢,还是保持原有比例呢。就酱。
明天再放其他的代码文件 =,= 好累,累觉不爱了。
待续...