前言
这篇文章的目的是了解OpenGL中图形渲染的基本图元以及如何绘制基本图元,真正从原理入手,了解图形渲染的基本单位,这样才能更好的掌握OpenGL这门学科。
一、基本图元
OpenGL中所有的图形组成的基本单位只有7种:点、线、三角行、线带、线环、三角形带、三角形环。如图1基本图元所示,图元名称及对应的枚举类型。二、绘制一个三角形
笔者用的是Mac,IDE是Xcode。用OpenGL绘制三角形需要一个工具包GLTools,文章末尾会提供demo地址,里面有这个包。话不多说,我们从代码层面上开始。
1.全局变量
// 批次类,这个是工具GLTools提供,存放顶点坐标并且绘制。
GLBatch triangleBatch;
// 着色器程序,工具内部已经实现,可以暂时直接使用,在OpenGL阶段会自己实现顶点和片元着色器。
GLShaderManager shaderManager;
2.main函数
// 设置当前工作区,window平台会默认当前工作区是OpenGL的上下文,Mac需要手动设置。
gltSetWorkingDirectory(argv[0]);
// 初始化glut,需要include
glutInit(&argc, argv);
// 初始化展示模式,默认是GLUT_RGBA
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(600, 400);
glutCreateWindow("三角形");
// 当前窗口的尺寸发生改变时会调用,changeSize是一个函数指针,可以理解成回调。
glutReshapeFunc(changeSize);
// 注册一个函数用来执行渲染,当调用glutPostRedisplay函数时这个函数会触发调用。
glutDisplayFunc(render);
// 固定写法,初始化glew,在其官网上有说明。
GLenum err = glewInit();
if (err != GLEW_OK) {
printf("error");
return 1;
}
// 设置一些参数,比如设置清屏颜色,确定顶点坐标之类。
setup();
// 开启runloop,类似iOS中的runloop。
glutMainLoop();
3.其余几个函数
void changeSize(int w, int h){
// 设置视口,OpenGL ES中也需要设置,可以理解成设置画布的大小。相当于UIView的frame。
glViewport(0, 0, w, h);
}
void setup(){
// 设置清空屏幕的颜色
glClearColor(1, 1, 1, 0);
// 用上面的颜色清空 颜色缓冲区GL_COLOR_BUFFER_BIT,如果需要用到深度测试,则还需要清空深度缓冲区GL_DEPTH_BUFFER_BIT
// 执行顺序不能打乱,可以试下颠倒顺序后的效果
// 放在render里面和这里都可以,render里面可能会清空多次
// glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 初始化着色器
shaderManager.InitializeStockShaders();
// 三角形顶点,OpenGL中的画布都是以当前屏幕的中心点为原点,x,y的范围都是[-1, 1]。所以这里可以理解成百分比坐标。
GLfloat points[] = {
-0.5, -0.5, 0,
0.5, -0.5, 0,
0.0, 0.5, 0
};
// 开始,GL_TRIANGLES表示绘制三角形,3表示3个顶点
triangleBatch.Begin(GL_TRIANGLES, 3);
triangleBatch.CopyVertexData3f(points);
// 调用end方法后,triangleBatch中添加新的顶点就没有意义了。
triangleBatch.End();
}
详细的固定着色器理解可以参考这篇文章:
https://www.jianshu.com/p/8005dcd0dec7
void render(){
// 清空缓冲区,因为OpenGL是状态机,不清空会使用上一次存留渲染的图形。
glClear(GL_COLOR_BUFFER_BIT);
GLfloat color[] = {0, 0, 0, 0};
// 着色器,这里的着色器是固定存储着色器,GLTools工具已经实现,
// 有多种着色器可以选择,这里用的是默认着色器。
shaderManager.UseStockShader(GLT_SHADER_IDENTITY, color);
// 绘制
triangleBatch.Draw();
// 提交缓冲区,三角形在当前缓冲区已经绘制完毕,提交给前台也就是GPU进行渲染、显示。
glutSwapBuffers();
}
以上是绘制一个三角形的全部代码。下面我们看一下,图形的3D变换。
三、视图变换
这一节我们对图形进行变换,让它动起来。
1.几个变量
在2中已经设置了批次类triangleBatch和着色器shaderManager,进行3D变换还需要以下几个变量:
// 透视投影
GLFrustum viewFrustum;
// 观察者
GLFrame objectFrame;
在计算机界有一句经典名言:如果一个东西看起来像什么,那么它就是什么。为什么要插入这样一句话,因为在计算机图形中是没有3D图形的,所谓的3D是模拟的3D效果,让它看起来像3D。
我们可以这样理解,在一张白纸上面画一个看上去像3D的画,实际上它是通过一些远近的偏差,让画看上去像3D,其实OpenGL也是这样的。而viewFrustum是用来设置投影的方向远近的,与此同时,想要达到3D效果,还需要开启深度测试,在上一篇文章中已经讲到过深度测试,其本质是让空间坐标z有意义。
2.矩阵
图形的3D变换归根结底就是对图形的顶点进行了矩阵计算。
2.1 在changeSize中加入
viewFrustum.SetPerspective(20, float(w)/float(h), 1, 100);
设置viewFrustum,在它的内部有保存一个矩阵,而这个矩阵用来参与模型视图矩阵的计算。
2.2 开启深度测试
在render中加入glEnable(GL_DEPTH_TEST) ;这行代码。
2.3 增加一个函数specialKey
这个函数是用来监听键盘上的输入,比如你按下了上下左右的按键。
在main函数中注册监听glutSpecialFunc(specialKey);
void specialKey(int key, int x, int y) {
// 监听按键 上下左右 设置观察矩阵,这里的objectFrame可以理解成你的头,上下左右时你的头绕着一个物体进行旋转,这样你从不同的角度观察到的现象不同。
if (key == GLUT_KEY_UP) {
objectFrame.RotateWorld(m3dDegToRad(5), 1, 0, 0);
}
if (key == GLUT_KEY_DOWN) {
objectFrame.RotateWorld(m3dDegToRad(-5), 1, 0, 0);
}
if (key == GLUT_KEY_LEFT) {
objectFrame.RotateWorld(m3dDegToRad(5), 0, 1, 0);
}
if (key == GLUT_KEY_RIGHT) {
objectFrame.RotateWorld(m3dDegToRad(-5), 0, 1, 0);
}
glutPostRedisplay();
}
2.4 替换render中的着色器
M3DMatrix44f mCamera;
// 将观察矩阵赋值到mCamera矩阵中
objectFrame.GetMatrix(mCamera);
M3DMatrix44f modelViewProjection;
// 透视投影矩阵和观察矩阵相乘
m3dMatrixMultiply44(modelViewProjection, viewFrustum.GetProjectionMatrix(), mCamera);
// 平面着色器,不同于上述中的 GLT_SHADER_IDENTITY 默认着色器
shaderManager.UseStockShader(GLT_SHADER_FLAT, modelViewProjection, color);
每次改变objectFrame之后重新绘制,透视投影矩阵和观察矩阵相乘得到顶点变换的最终变换矩阵,在着色器内部进行处理,这样就能看到图形的3D变换了。然而这种两个矩阵相乘的方式并不是一个好的方式,因为大多时候视图变换没有如此简单,需要我们借助一些工具去处理矩阵的计算。下面我们一起来看看模型视图矩阵堆栈。
四、模型视图矩阵堆栈
从上述例子中,我们可以看出,不使用模型视图矩阵堆栈我们是可以进行3D变换的。而模型视图矩阵堆栈仅仅只是方便我们处理视图变换,我们可以不去考虑矩阵相乘这个过程,而更加专注如何去变换视图。这里引入一个OpenGL中的常用词MVP,这可不是MVP设计模式,而是模型视图矩阵
1.增加变量
// 视图矩阵堆栈
GLMatrixStack modelViewMatrix;
// 投影矩阵堆栈
GLMatrixStack projectionMatrix;
// 用于管理模型视图矩阵
GLGeometryTransform transformPipeLine;
2.管理模型视图矩阵
2.1 changeSize中将透视投影绑定到transformPipeLine
void changeSize(int w, int h) {
glViewport(0, 0, w, h);
if (h==0) {
h = 1;
}
viewFrustum.SetPerspective(20, float(w)/float(h), 1, 100);
// 将透视投影矩阵加载到projection中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
// 固定管线transformpipeLine绑定视图矩阵和投影矩阵
transformPipeLine.SetMatrixStacks(modelViewMatrix, projectionMatrix);
}
2.2 渲染
// 在内部实现方法中可以看到,modelViewMatrix栈顶增加一个矩阵objectFrame
modelViewMatrix.PushMatrix(objectFrame);
// 在固定管线中取出MVP矩阵时,会计算modelView和projection两个矩阵的乘积,原理和上述手动对视图和投影矩阵相乘一样。
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeLine.GetModelViewProjectionMatrix(), color);
triangleBatch.Draw();
// 因为上面对矩阵栈进行了push操作,栈顶就是push之后的那个矩阵objectFrame,
// 这里pop一次,让栈还原到原来的状态。之所以要这样操作,必须始终牢记一点,OpenGL是状态机,
// 这一次的操作会影响下一次的渲染结果。而这里出栈是为了不影响下一次的渲染效果。
modelViewMatrix.PopMatrix();
至于矩阵堆栈的进出,个人认为实在没有太大的必要在这里讲解,因为清楚栈的结果的小伙伴都清楚。如果还不能理解,可以去百度找找相关解释,这里不再赘述。
另外再次推荐小伙伴《3D数学基础:图形与游戏开发》这本书,可以通读前9章,了解一下图形变换的原理,实际上就是矩阵,至于为什么会用到矩阵来表示变换,笔者表示很遗憾,不知道,毕竟超出了笔者的认知。
总结
这篇文章主要是带小伙伴了解一下OpenGL的基本图元,已经图形变换的原理--矩阵。有关更多渲染技巧的文章后面会慢慢推出,附上本文的demo地址:
https://github.com/zhaoguyixia/OpenGL.git。