一、简介
这个里主要演示一些常见图元的绘制: 点、线、线环、六边形、环带以及自带的’🍩‘的绘制.
大致流程为:
- 配置键盘回调,通过空格键来切换各个图形的绘制
- 配置特殊按键的回调,通过上下左右来控制图像的旋转
- 配置绘制回调
- 初始化配置,通过管线使用两个矩阵(物体模型矩阵与投影矩阵), 每一个图像的绘制都对应一个批次类,为其提供顶点数据
- 渲染回调里进行矩阵堆栈操作, 在其栈顶矩阵上进行矩阵运算
- 绘制模型视图矩阵和投影矩阵
- 批次类开始绘制
- 还原矩阵堆栈,交换缓冲区
此处使用的主要库为:
- GLTools
- glut
二、键盘对图像的控制
2.1、空格回调
首先来看一下空格键的控制, 通过空格来重新渲染图像达到切换图像的目的. 我们用一个变量来记录当前是应该绘制什么图像
注册回调:
glutKeyboardFunc(KeyPressFunc);//键盘回调
回调实现里我们通过变量nStep来记录当前应该绘制什么图像, 并且将窗口标题改成其对应绘制的图像名字,再重新渲染
void KeyPressFunc(unsigned char key, int x, int y)
{
//32对应空格键的ASCII码, 具体自行查阅
if (key == 32) {
nStep++;
if (nStep > 6) {
nStep = 0;
}
}
//绘制7种图像
switch (nStep) {
case 0:
glutSetWindowTitle("GL_POINTES");
break;
case 1:
glutSetWindowTitle("GL_LINES");
break;
case 2:
glutSetWindowTitle("GL_LINE_STRIP");
break;
case 3:
glutSetWindowTitle("GL_LINE_LOOP");
break;
case 4:
glutSetWindowTitle("GL_TRIANGLES");
break;
case 5:
glutSetWindowTitle("GL_TRIANGLE_STRIP");
break;
case 6:
glutSetWindowTitle("GL_TRIANGLE_FAN");
break;
}
//重新渲染
glutPostRedisplay();
}
2.2、特殊按键回调
针对特殊按键的处理,就需要注册特殊按键的回调:
glutSpecialFunc(SpecialKeys);
回调实现里通过识别上下左右来对图像进行旋转操作, 通过修改物体矩阵来实现物体旋转, 并重新渲染:
void SpecialKeys(int key, int x, int y)
{
//旋转这个物体
//按 ‘上‘ 绕x轴旋转
if (key == GLUT_KEY_UP) {
objectFrame.RotateWorld(m3dDegToRad(-5.0f), 1.0f, 0.0f, 0.0f);
}
//按 ‘下‘ 绕x轴旋转
if (key == GLUT_KEY_DOWN) {
objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
}
//按 ‘左‘ 绕y轴旋转
if (key == GLUT_KEY_LEFT) {
objectFrame.RotateWorld(m3dDegToRad(-5.0f), 0.0f, 1.0f, 0.0f);
}
//按 ‘右‘ 绕y轴旋转
if (key == GLUT_KEY_RIGHT) {
objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
}
//重新渲染
glutPostRedisplay();
}
三、图像的初始化配置
想要绘制一个图像,就需要直到图像的顶点数据来对其进行绘制, 所以在初始化函数里需要做的几件事情:
- 设置窗口背景色
- 初始化着色管理器
- 通过管道来使用两个矩阵堆栈
- 设置观察方式
- 处理顶点数据
- 提交批次类
先来创建一些批次类, 分别对应需要绘制的各个图像:
//对应七种图形
GLBatch pointBatch;
GLBatch lineBatch;
GLBatch lineStripBatch;
GLBatch lineLoopBatch;
GLBatch triangleBatch;
GLBatch triangleStripBatch;
GLBatch triangleFanBatch;
创建金字塔、六边形、环带的顶点数据都可以通过数学坐标计算来得到, 不要拘泥于顶点数据的计算,此工作一般是由其他岗位的工程师来进行提供
void SetupRC()
{
//设置背影颜色
glClearColor(0.7f,0.7f,0.7f,1.0f);
//初始化着色管理器
shaderManager.InitializeStockShaders();
//设置变换管线以使用两个矩阵堆栈
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
//设置观察者位置
cameraFrame.MoveForward(-15.0f);
//顶点数据(物体坐标系) 转换为 规范坐标系
GLfloat vCoast[9] = {
3, 3, 0,
0, 3, 0,
3, 0, 0
};
//提交 ’点‘ 批次类
pointBatch.Begin(GL_POINTS, 3);
pointBatch.CopyVertexData3f(vCoast);
pointBatch.End();
//提交 ’线‘ 批次类
lineBatch.Begin(GL_LINES, 3);
lineBatch.CopyVertexData3f(vCoast);
lineBatch.End();
//提交 ’线段‘ 批次类
lineStripBatch.Begin(GL_LINE_STRIP, 3);
lineStripBatch.CopyVertexData3f(vCoast);
lineStripBatch.End();
//提交 ’线环‘ 批次类
lineLoopBatch.Begin(GL_LINE_LOOP, 3);
lineLoopBatch.CopyVertexData3f(vCoast);
lineLoopBatch.End();
//提交 ’金字塔‘ 批次类
// 设置 ‘金字塔’ 顶点数据
GLfloat vPyramid[12][3] = {
-2.0f, 0.0f, -2.0f,
2.0f, 0.0f, -2.0f,
0.0f, 4.0f, 0.0f,
2.0f, 0.0f, -2.0f,
2.0f, 0.0f, 2.0f,
0.0f, 4.0f, 0.0f,
2.0f, 0.0f, 2.0f,
-2.0f, 0.0f, 2.0f,
0.0f, 4.0f, 0.0f,
-2.0f, 0.0f, 2.0f,
-2.0f, 0.0f, -2.0f,
0.0f, 4.0f, 0.0f
};
triangleBatch.Begin(GL_TRIANGLES, 12);
triangleBatch.CopyVertexData3f(vPyramid);
triangleBatch.End();
//提交 ’六边形‘ 批次类
// 三角形扇形--六边形
GLfloat vPoints[100][3];
int nVerts = 0;
//半径
GLfloat r = 3.0f;
//原点(x,y,z) = (0,0,0);
vPoints[nVerts][0] = 0.0f;
vPoints[nVerts][1] = 0.0f;
vPoints[nVerts][2] = 0.0f;
//M3D_2PI 就是2Pi 的意思,就一个圆的意思。 绘制圆形
for(GLfloat angle = 0; angle < M3D_2PI; angle += M3D_2PI / 6.0f) {
//数组下标自增(每自增1次就表示一个顶点)
nVerts++;
/*
弧长=半径*角度,这里的角度是弧度制,不是平时的角度制
既然知道了cos值,那么角度=arccos,求一个反三角函数就行了
*/
//x点坐标 cos(angle) * 半径
vPoints[nVerts][0] = float(cos(angle)) * r;
//y点坐标 sin(angle) * 半径
vPoints[nVerts][1] = float(sin(angle)) * r;
//z点的坐标
vPoints[nVerts][2] = -0.5f;
}
// 结束扇形 前面一共绘制7个顶点(包括圆心)
//添加闭合的终点
//课程添加演示:屏蔽177-180行代码,并把绘制节点改为7.则三角形扇形是无法闭合的。
nVerts++;
vPoints[nVerts][0] = r;
vPoints[nVerts][1] = 0;
vPoints[nVerts][2] = 0.0f;
triangleFanBatch.Begin(GL_TRIANGLE_FAN, 8);
triangleFanBatch.CopyVertexData3f(vPoints);
triangleFanBatch.End();
//提交 ’环带‘ 批次类
//顶点下标
int iCounter = 0;
//半径
GLfloat radius = 3.0f;
//从0度~360度,以0.3弧度为步长
for(GLfloat angle = 0.0f; angle <= (2.0f*M3D_PI); angle += 0.3f)
{
//或许圆形的顶点的X,Y
GLfloat x = radius * sin(angle);
GLfloat y = radius * cos(angle);
//绘制2个三角形(他们的x,y顶点一样,只是z点不一样)
vPoints[iCounter][0] = x;
vPoints[iCounter][1] = y;
vPoints[iCounter][2] = -0.5;
iCounter++;
vPoints[iCounter][0] = x;
vPoints[iCounter][1] = y;
vPoints[iCounter][2] = 0.5;
iCounter++;
}
// 关闭循环
//结束循环,在循环位置生成2个三角形
vPoints[iCounter][0] = vPoints[0][0];
vPoints[iCounter][1] = vPoints[0][1];
vPoints[iCounter][2] = -0.5;
iCounter++;
vPoints[iCounter][0] = vPoints[1][0];
vPoints[iCounter][1] = vPoints[1][1];
vPoints[iCounter][2] = 0.5;
iCounter++;
// GL_TRIANGLE_STRIP 共用一个条带(strip)上的顶点的一组三角形
triangleStripBatch.Begin(GL_TRIANGLE_STRIP, iCounter);
triangleStripBatch.CopyVertexData3f(vPoints);
triangleStripBatch.End();
}
四、渲染处理
提交了批次类后, 在渲染回调里就可以来渲染图像了. 这里就涉及到矩阵堆栈. 试想一下, 当我们需要调整物体视图矩阵进行一些操作的时候, 如果是直接在视图矩阵三进行操作,那么这个操作将会是不可逆的, 每一次图像的渲染将会在新的矩阵上进行操作, 这样不仅降低了计算机的处理效率, 也不方便我们的后续开发.
那么通常的做法就是, 用一个特殊的区域来存放一个拷贝的矩阵数据, 所有的矩阵操作都是在这个拷贝的矩阵上进行操作, 操作结束渲染成功后丢弃拷贝的矩阵数据, 下一次渲染继续拷贝一份新的原始矩阵数据来进行操作. 这样就不会对原始的图像数据造成影响.这种操作就是用到矩阵堆栈来进行的.
(在iOS开发中, 每一个类都有元类, 这元类就是最原始的数据, 不会被修改的.)
4.1、矩阵堆栈介绍
矩阵堆栈就是内存中用来存放矩阵数据的特殊区域.
在一些简单的模型进行处理构成复杂的模型, 使用矩阵堆栈可以让简单模型在处理的过程中经过多个变换后, 简单模型与新的模型之间保持着相互联系又独立的特性. 这对整个结构来说是十分有利的.
借用大佬的图,堆栈信息的变化如下:
4.2、矩阵堆栈的使用
首先创建一个矩阵堆栈,我们可以通过矩阵堆栈来进行一些图像的操作:
GLMatrixStack modelViewMatrix;//模型视图矩阵, 用于操作变换
GLMatrixStack projectionMatrix;//投影矩阵
一般在配置矩阵堆栈是只需要初始化一次的, 所以我们可以在上文中的 void SetupRC() 初始化方法中再添加一个矩阵堆栈的出初始化(投影矩阵的初始化需要设置一些特定参数):
//设置透视投影矩阵
/**
* 参数:
* 眼睛打开的角度
* 纵横比
* 最小远距离
* 最大远距离
*/
GLFrustum viewFrustum = viewFrustum.SetPerspective(35.0f, float(w)/float(h), 1.0f, 500.0f);
//装载到投影矩阵上
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
//设置模型视图矩阵 -- 加载一个单元矩阵(对角线为1其他为0的矩阵)
modelViewMatrix.LoadIdentity();
当我们需要对图像进行一些操作,例如旋转的时候, 我们就可以对其压栈: 将自身的矩阵进行一次拷贝, 然后存放进矩阵堆栈中:
//压栈 -- 在单元矩阵上再放一个单元矩阵 -- 方便回退等操作
modelViewMatrix.PushMatrix();
等图形绘制完成了之后我们就可以对其出栈,弃用掉处理过后的单元矩阵
//图像 已经绘制,还原视图矩阵。方便下次绘制. 此时视图矩阵中只有一个最开始的单元矩阵
modelViewMatrix.PopMatrix();
4.3、利用矩阵堆栈进行图像绘制
现在明白矩阵堆栈的使用了之后, 在渲染回调里可以进行一次压栈, 再进行操作:
- 压栈后使用的为栈顶矩阵
- 绘制图像需要观察者矩阵
- 需要通过观察者Frame来构建观察者矩阵
- 将得到的观察者矩阵与栈顶矩阵相乘得到新的观察者的矩阵
- 绘制图像需要物体矩阵
- 通过frame构建物体矩阵
- 将带观察者的矩阵与物体矩阵相乘,得到新的物体矩阵
- 此时栈顶即为新的物体矩阵
- 在初始化中已经通过管线来绑定了物体矩阵堆栈和投影矩阵堆栈, 所以通过管线将模型视图矩阵和投影矩阵提交给着色管理器
- 根据当前nStep变量来确定需要哪个批次类来进行绘制
- 出栈矩阵堆栈, 交换缓冲区
注: 此处绘制多面图形,需要通过自定义 DrawWireFramedBatch() 方法来绘制边, 放在下篇章.
void RenderScene(void)
{
//清除一个或一组特定的缓冲区
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);
//压栈 -- 在单元矩阵上再放一个单元矩阵 -- 方便回退
modelViewMatrix.PushMatrix();
//通过frame构建观察者矩阵
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
//矩阵乘以矩阵堆栈的顶部矩阵,相乘的结果随后简存储在堆栈的顶部
//栈顶的单元矩阵 x 观察者矩阵 = 新的观察者矩阵
modelViewMatrix.MultMatrix(mCamera);
//通过frame构建物体矩阵
M3DMatrix44f mObjectFrame;
objectFrame.GetMatrix(mObjectFrame);
//新的观察者矩阵 x 物体矩阵 = 新的物体矩阵
modelViewMatrix.MultMatrix(mObjectFrame);
//绘制 模型视图矩阵(观察矩阵、物体变换矩阵)和投影矩阵
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
switch (nStep) {
case 0:
//绘制点 -- 设置点大小
glPointSize(4.0f);
pointBatch.Draw();
glPointSize(1.0f);
break;
case 1:
//绘制线 -- 设置线宽度
glLineWidth(4.0f);
lineBatch.Draw();
glLineWidth(1.0f);
break;
case 2:
//绘制线环 -- 设置线宽度
glLineWidth(4.0f);
lineLoopBatch.Draw();
glLineWidth(1.0f);
break;
case 3:
//绘制线段 -- 设置线宽度
glLineWidth(4.0f);
lineStripBatch.Draw();
glLineWidth(1.0f);
break;
case 4:
//绘制‘金字塔’ -- 绘制边
DrawWireFramedBatch(&triangleBatch);
break;
case 5:
//绘制‘六边形’ -- 绘制边
DrawWireFramedBatch(&triangleFanBatch);
break;
case 6:
//绘制‘环带’ -- 绘制边
DrawWireFramedBatch(&triangleStripBatch);
break;
default:
break;
}
//图像 已经绘制,还原视图矩阵。方便下次绘制. 此时视图矩阵中只有一个最开始的单元矩阵
modelViewMatrix.PopMatrix();
//交换缓冲区
glutSwapBuffers();
}
这里运行程序就可以看到:
五、系统API绘制 ‘🍩’
上面我们都是自己通过手动来添加顶点数据进行绘制图像. 在glut中为我们提供了一些已经处理好的图案. 通过以下API:
GLTriangleBatch torusBatch;
//创建一个🍩
//void gltMakeTorus(GLTriangleBatch& torusBatch, GLfloat majorRadius, GLfloat minorRadius, GLint numMajor, GLint numMinor);
//参数1:GLTriangleBatch 容器帮助类
//参数2:外边缘半径
//参数3:内边缘半径
//参数4、5:主半径和从半径的细分单元数量
gltMakeTorus(torusBatch, 1.0f, 0.3f, 52, 26);`
即可创建出一个🍩的批次类, 在渲染回调里进行批次类绘制就可以得到这个图案:
torusBatch.Draw();
在这些图案我们都是使用的平面着色器(GLT_SHADER_FLAT), 如果使用光源着色器(GLT_SHADER_DEFAULT_LIGHT)就会出一些意想不到的问题, 有兴趣的同学可以将
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vRed);
替换成:
GLfloat vRed[] = { 1.0f, 0.0f, 0.0f, 1.0f };
shaderManager.UseStockShader(GLT_SHADER_DEFAULT_LIGHT, transformPipeline.GetModelViewMatrix(), transformPipeline.GetProjectionMatrix(), vRed);
自行体验一下.
问题所在将在下个篇章分析.