iOS视觉(四) -- 常见图元绘制

595 阅读10分钟

一、简介

这个里主要演示一些常见图元的绘制: 点、线、线环、六边形、环带以及自带的’🍩‘的绘制.

大致流程为:

  1. 配置键盘回调,通过空格键来切换各个图形的绘制
  2. 配置特殊按键的回调,通过上下左右来控制图像的旋转
  3. 配置绘制回调
  4. 初始化配置,通过管线使用两个矩阵(物体模型矩阵与投影矩阵), 每一个图像的绘制都对应一个批次类,为其提供顶点数据
  5. 渲染回调里进行矩阵堆栈操作, 在其栈顶矩阵上进行矩阵运算
  6. 绘制模型视图矩阵和投影矩阵
  7. 批次类开始绘制
  8. 还原矩阵堆栈,交换缓冲区

此处使用的主要库为:

  1. GLTools
  2. 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();
}

三、图像的初始化配置

想要绘制一个图像,就需要直到图像的顶点数据来对其进行绘制, 所以在初始化函数里需要做的几件事情:

  1. 设置窗口背景色
  2. 初始化着色管理器
  3. 通过管道来使用两个矩阵堆栈
  4. 设置观察方式
  5. 处理顶点数据
  6. 提交批次类

先来创建一些批次类, 分别对应需要绘制的各个图像:

//对应七种图形
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、利用矩阵堆栈进行图像绘制

现在明白矩阵堆栈的使用了之后, 在渲染回调里可以进行一次压栈, 再进行操作:

  1. 压栈后使用的为栈顶矩阵
  2. 绘制图像需要观察者矩阵
    1. 需要通过观察者Frame来构建观察者矩阵
    2. 将得到的观察者矩阵与栈顶矩阵相乘得到新的观察者的矩阵
  3. 绘制图像需要物体矩阵
    1. 通过frame构建物体矩阵
    2. 将带观察者的矩阵与物体矩阵相乘,得到新的物体矩阵
    3. 此时栈顶即为新的物体矩阵
  4. 在初始化中已经通过管线来绑定了物体矩阵堆栈和投影矩阵堆栈, 所以通过管线将模型视图矩阵和投影矩阵提交给着色管理器
  5. 根据当前nStep变量来确定需要哪个批次类来进行绘制
  6. 出栈矩阵堆栈, 交换缓冲区

注: 此处绘制多面图形,需要通过自定义 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);

自行体验一下.

问题所在将在下个篇章分析.

六、附图--图元连接方式