前言
上篇文章OpenGL(五)- 基础渲染概念理解 介绍了下基础渲染相关概念,本文将在此基础上通过代码进一步理解渲染流程。
代码实现
首先看一个基础图形绘制示例。
#include "primitives.h"
#include <GLTools.h>
#include <GLShaderManager.h>
#ifndef FREEGLUT_STATIC
#define FREEGLUT_STATIC
#endif
#include <GL/glut.h>
#include <GLMatrixStack.h>
#include <GLFrame.h>
#include <GLFrustum.h>
#include <GLBatch.h>
#include <GLGeometryTransform.h>
#include <math.h>
namespace primitives
{
static GLShaderManager shaderManager; // 全局着色器管理器实例
static GLMatrixStack modelViewMatrix; // 模型视图矩阵栈,处理物体旋转、平移和相机位置
static GLMatrixStack projectionMatrix; // 投影矩阵栈,处理透视或正交投影
static GLFrame cameraFrame; // 代表观察者的位置
static GLFrame objectFrame; // 代表被观察物体的位置和旋转状态
static GLFrustum viewFrustum; // 管理和计算透视投影矩阵
static GLBatch pointBatch;
static GLBatch lineBatch;
static GLBatch lineStripBatch;
static GLBatch lineLoopBatch;
static GLBatch triangleBatch;
static GLBatch triangleStripBatch;
static GLBatch triangleFanBatch;
static GLGeometryTransform transformPipeline; // 变换管线,负责将模型视图矩阵和投影矩阵节后,生成最终MVP矩阵传递给着色器
static M3DMatrix44f shadowMatrix;
static GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };
static GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };
static int nStep = 0;
void SetupRC()
{
glClearColor(0.7f, 0.7f, 0.7f, 1.0f);
shaderManager.InitializeStockShaders();
glEnable(GL_DEPTH_TEST); // 启用深度测试
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
cameraFrame.MoveForward(–15.0f); // 将相机向后移动15个单位,以便能看到原点附近的物体
// 弗洛里达点阵
GLfloat vCoast[24][3] = {
{2.80, 1.20, 0.0}, {2.0, 1.20, 0.0},
{2.0, 1.08, 0.0}, {2.0, 1.08, 0.0},
{0.0, 0.80, 0.0}, {–.32, 0.40, 0.0},
{–.48, 0.2, 0.0}, {–.40, 0.0, 0.0},
{–.60, –.40, 0.0 }, {–.80, –.80, 0.0 },
{–.80, –1.4, 0.0 }, {–.40, –1.60, 0.0 },
{0.0, –1.20, 0.0 }, { .2, –.80, 0.0 },
{.48, –.40, 0.0 }, {.52, –.20, 0.0 },
{.48, .20, 0.0 }, {.80, .40, 0.0 },
{1.20, .80, 0.0 }, {1.60, .60, 0.0 },
{2.0, .60, 0.0 }, {2.2, .80, 0.0 },
{2.40, 1.0, 0.0 }, {2.80, 1.0, 0.0 }
};
// 初始化批次实例
pointBatch.Begin(GL_POINTS, 24);
pointBatch.CopyVertexData3f(vCoast);
pointBatch.End();
// 一堆线段加载
lineBatch.Begin(GL_LINES, 24);
lineBatch.CopyVertexData3f(vCoast);
lineBatch.End();
// 单个线段加载
lineStripBatch.Begin(GL_LINE_STRIP, 24);
lineStripBatch.CopyVertexData3f(vCoast);
lineStripBatch.End();
// 单线但首尾相连
lineLoopBatch.Begin(GL_LINE_LOOP, 24);
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;
vPoints[nVerts][0] = –0.0f;
vPoints[nVerts][1] = –0.0f;
vPoints[nVerts][2] = –0.0f;
for (GLfloat angle = 0; angle < M3D_2PI; angle += M3D_2PI / 6.0f)
{
nVerts++;
vPoints[nVerts][0] = float(cos(angle)) * r;
vPoints[nVerts][1] = float(sin(angle)) * r;
vPoints[nVerts][2] = –0.5f;
}
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;
for (GLfloat angle = 0; angle <= (2.0f * M3D_PI); angle += 0.3f)
{
GLfloat x = radius * sin(angle);
GLfloat y = radius * cos(angle);
// 指定点的z轴坐标移动
vPoints[iCounter][0] = x;
vPoints[iCounter][1] = y;
vPoints[iCounter][2] = –0.5f;
iCounter++;
vPoints[iCounter][0] = x;
vPoints[iCounter][1] = y;
vPoints[iCounter][2] = 0.5f;
iCounter++;
}
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++;
triangleStripBatch.Begin(GL_TRIANGLE_STRIP, iCounter);
triangleStripBatch.CopyVertexData3f(vPoints);
triangleStripBatch.End();
}
void DrawWireFramedBatch(GLBatch *pBatch)
{
// 将批次绘制成纯绿色
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
pBatch->Draw();
// 绘制黑色轮廓
glPolygonOffset(–1.0f, –1.0f);
glEnable(GL_POLYGON_OFFSET_FILL); // 启用多边形偏移
// 绘制抗锯齿线条
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// 绘制几何图形的黑色线框版本
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glLineWidth(2.5f);
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
pBatch->Draw();
// 把所有东西都放回原处
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glDisable(GL_POLYGON_OFFSET_LINE);
glLineWidth(1.0f);
glDisable(GL_BLEND);
glDisable(GL_LINE_SMOOTH);
}
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
modelViewMatrix.PushMatrix(); // 保存当前矩阵状态
// 获取相机矩阵并乘入模型视图矩阵
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
modelViewMatrix.MultMatrix(mCamera);
// 获取物体旋转矩阵并乘入
M3DMatrix44f mObjectFrame;
objectFrame.GetMatrix(mObjectFrame);
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(2.0f);
lineBatch.Draw();
glLineWidth(1.0f);
break;
case 2:
glLineWidth(2.0f);
lineStripBatch.Draw();
glLineWidth(1.0f);
break;
case 3:
glLineWidth(2.0f);
lineLoopBatch.Draw();
glLineWidth(1.0f);
break;
case 4:
DrawWireFramedBatch(&triangleBatch);
break;
case 5:
DrawWireFramedBatch(&triangleFanBatch);
break;
case 6:
DrawWireFramedBatch(&triangleStripBatch);
break;
}
modelViewMatrix.PopMatrix();
glutSwapBuffers();
}
void SpecialKeys(int key, int x, int y)
{
if (key == GLUT_KEY_UP)
objectFrame.RotateWorld(m3dDegToRad(–5.0f), 0.0f, 1.0f, 0.0f);
if (key == GLUT_KEY_DOWN)
objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
if (key == GLUT_KEY_LEFT)
objectFrame.RotateWorld(m3dDegToRad(–5.0f), 0.0f, 1.0f, 0.0f);
if (key == GLUT_KEY_RIGHT)
objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
glutPostRedisplay();
}
void KeyPressFunc(unsigned char key, int x, int y)
{
if (key == 32)
{
nStep++;
if (nStep > 6)
nStep = 0;
}
switch (nStep)
{
case 0:
glutSetWindowTitle("GL_POINTS");
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();
}
void ChangeSize(int w, int h)
{
glViewport(0, 0, w, h);
// 1. 设置透视参数:
// 35.0f: 垂直视野角度为 35 度
// float(w)/float(h): 纵横比
// 1.0f: 近裁剪面距离
// 500.0f: 远裁剪面距离
viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
// 2. 将计算好的投影矩阵加载到投影矩阵栈中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
modelViewMatrix.LoadIdentity();
}
int primitives_test(int argc, char* argv[])
{
gltSetWorkingDirectory(argv[0]);
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(800, 600);
glutCreateWindow("GL_POINTS");
glutReshapeFunc(ChangeSize);
glutKeyboardFunc(KeyPressFunc);
glutSpecialFunc(SpecialKeys);
glutDisplayFunc(RenderScene);
GLenum err = glewInit();
if (GLEW_OK != err)
{
// fprintf(stderr, "GLEW Error: %s\\n", glewGetErrorString(err));
return 1;
}
SetupRC();
glutMainLoop();
return 0;
}
}
效果预览: 
这里看下代码如何实现的。
首先看下最前面定义的一些全局变量:
static GLShaderManager shaderManager; // 全局着色器管理器实例
static GLMatrixStack modelViewMatrix; // 模型视图矩阵栈,处理物体旋转、平移和相机位置
static GLMatrixStack projectionMatrix; // 投影矩阵栈,处理透视或正交投影
static GLFrame cameraFrame; // 代表观察者的位置
static GLFrame objectFrame; // 代表被观察物体的位置和旋转状态
static GLFrustum viewFrustum; // 管理和计算透视投影矩阵
static GLBatch pointBatch; // 顶点数据批次类
static GLBatch lineBatch;
static GLBatch lineStripBatch;
static GLBatch lineLoopBatch;
static GLBatch triangleBatch;
static GLBatch triangleStripBatch;
static GLBatch triangleFanBatch;
static GLGeometryTransform transformPipeline; // 变换管线,负责将模型视图矩阵和投影矩阵节后,生成最终MVP矩阵传递给着色器
static GLfloat vGreen[] = { 0.0f, 1.0f, 0.0f, 1.0f };
static GLfloat vBlack[] = { 0.0f, 0.0f, 0.0f, 1.0f };
static int nStep = 0;
一个全局着色器管理实例,2个矩阵栈用于管理维护模型视图矩阵和投影矩阵,2个框架对象用来记录物体和观察者的位置及状态,一个观察者视角管理实例,然后就是一些顶点数据批次类,最后是一个封装的变换管线,用来生成MVP(模型视图投影矩阵)传递给服务端的着色器。
接着看main实现,为方便后续调用,在源码基础代码上重新实现了一下,源码的main改为primitives_test。
在primitives_test中可以看到标准的初始化流程:
int primitives_test(int argc, char* argv[])
{
gltSetWorkingDirectory(argv[0]); // 设置工作路径
glutInit(&argc, argv); // glut初始化
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL);
glutInitWindowSize(800, 600); // 初始化窗口
glutCreateWindow("GL_POINTS");
glutReshapeFunc(ChangeSize); // 回调绑定
glutKeyboardFunc(KeyPressFunc);
glutSpecialFunc(SpecialKeys);
glutDisplayFunc(RenderScene);
GLenum err = glewInit(); // glew初始化
if (GLEW_OK != err)
{
// fprintf(stderr, "GLEW Error: %s\\n", glewGetErrorString(err));
return 1;
}
SetupRC(); // 渲染环境初始化
glutMainLoop(); // 进入窗口循环
return 0;
}
先是初始化glut、glew及窗口,然后初始化渲染相关环境,最后进入窗口循环。
接着看下SetupRC()中的初始化内容:
void SetupRC()
{
glClearColor(0.7f, 0.7f, 0.7f, 1.0f); // 设置背景颜色
shaderManager.InitializeStockShaders(); // 初始化GLShaderManager管理的着色器
glEnable(GL_DEPTH_TEST); // 启用深度测试
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
cameraFrame.MoveForward(–15.0f); // 将相机向后移动15个单位,以便能看到原点附近的物体
// 下面都是一些顶点构建初始化
// …
}
进入窗口循环后,就是对窗口事件的回调处理。
按键回调中只是简单的捕获空格(space)键按下,然后更新窗口标题,再请求重绘。
窗口尺寸变换的回调:
void ChangeSize(int w, int h)
{
glViewport(0, 0, w, h);
// 1. 设置透视参数:
// 35.0f: 垂直视野角度为 35 度
// float(w)/float(h): 纵横比
// 1.0f: 近裁剪面距离
// 500.0f: 远裁剪面距离
viewFrustum.SetPerspective(35.0f, float(w) / float(h), 1.0f, 500.0f);
// 2. 将计算好的投影矩阵加载到投影矩阵栈中
projectionMatrix.LoadMatrix(viewFrustum.GetProjectionMatrix());
modelViewMatrix.LoadIdentity();
}
这里通过viewFrustum对象设置坐标系类型为透视投影,然后将投影矩阵放到投影矩阵栈对象中进行管理。
接着看上下左右方向键按下后的回调处理:
void SpecialKeys(int key, int x, int y)
{
if (key == GLUT_KEY_UP)
objectFrame.RotateWorld(m3dDegToRad(–5.0f), 0.0f, 1.0f, 0.0f);
if (key == GLUT_KEY_DOWN)
objectFrame.RotateWorld(m3dDegToRad(5.0f), 1.0f, 0.0f, 0.0f);
if (key == GLUT_KEY_LEFT)
objectFrame.RotateWorld(m3dDegToRad(–5.0f), 0.0f, 1.0f, 0.0f);
if (key == GLUT_KEY_RIGHT)
objectFrame.RotateWorld(m3dDegToRad(5.0f), 0.0f, 1.0f, 0.0f);
glutPostRedisplay();
}
方向键按下后对物体进行相应的旋转变换。
最后看关键的绘制处理:
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
modelViewMatrix.PushMatrix(); // 保存当前矩阵状态
// 获取相机矩阵并乘入模型视图矩阵
M3DMatrix44f mCamera;
cameraFrame.GetCameraMatrix(mCamera);
modelViewMatrix.MultMatrix(mCamera);
// 获取物体旋转矩阵并乘入
M3DMatrix44f mObjectFrame;
objectFrame.GetMatrix(mObjectFrame);
modelViewMatrix.MultMatrix(mObjectFrame);
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack); // 使用平面着色器
// 根据当前选择绘制对应的图形
// …
modelViewMatrix.PopMatrix(); // 恢复绘制前的矩阵状态
glutSwapBuffers(); // 交换缓冲区上屏显示
}
绘制回调中线清理缓冲区,然后通过模型视图矩阵栈的压栈出栈保证绘制前后的状态。
然后进入实际绘制处理,首先获取相机(观察者)矩阵,即投影矩阵。
然后得到旋转后物体的模型视图矩阵。
接着使用着色器管理实例的平面着色器进行渲染,传入的数据有通过渲染管线实例生成的MVP(模型视图投影)矩阵,和颜色值,然后进入此前介绍的渲染流程。
最后交换缓冲区上屏显示。
这个过程就像是在组装一个变换链条:
简单来说:这段代码就是在告诉 OpenGL:“先把镜头摆好(Camera),再把物体摆好(Object),然后基于这个组合视角把物体画出来。”
最后再看下示例中的线框模式的绘制技巧:
void DrawWireFramedBatch(GLBatch *pBatch)
{
// 将批次绘制成纯绿色
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vGreen);
pBatch->Draw();
// 绘制黑色轮廓
glPolygonOffset(–1.0f, –1.0f);
glEnable(GL_POLYGON_OFFSET_FILL); // 启用多边形偏移
// 绘制抗锯齿线条
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
// 绘制几何图形的黑色线框版本
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glLineWidth(2.5f);
shaderManager.UseStockShader(GLT_SHADER_FLAT, transformPipeline.GetModelViewProjectionMatrix(), vBlack);
pBatch->Draw();
// 把所有东西都放回原处
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glDisable(GL_POLYGON_OFFSET_LINE);
glLineWidth(1.0f);
glDisable(GL_BLEND);
glDisable(GL_LINE_SMOOTH);
}
这个接口用于同时显示物体的填充颜色和黑色轮廓线:
思考
为什么需要2个GLMaxtrixStack?
在这个示例中,使用两个独立的 GLMatrixStack(modelViewMatrix 和 projectionMatrix)是为了模拟现代 OpenGL 渲染管线中矩阵变换的逻辑分离。
虽然它们最终会合并成一个 MVP 矩阵传给着色器,但在 CPU 端管理时将它们分开有以下核心原因:
职责分离(Separation of Concerns) 在 3D 图形学中,变换通常分为三个阶段,前两个阶段性质完全不同: • 模型视图矩阵 (Model-View Matrix): • 职责:描述物体在世界中的位置/旋转(Model)以及相机在哪里/看向哪(View)。 • 变化频率:极高。每一帧都可能因为用户输入(如键盘旋转物体)、动画或相机移动而改变。 • 层级关系:通常需要栈结构来处理复杂的场景图(Scene Graph)。例如:世界 -> 机器人身体 -> 机器人手臂 -> 机器人手指。你需要 PushMatrix 保存父级状态,绘制子级后再 PopMatrix 恢复。 • 投影矩阵 (Projection Matrix): • 职责:描述相机的镜头属性(如视野 FOV、纵横比 Aspect Ratio、近/远裁剪面)。它决定了 3D 空间如何映射到 2D 屏幕(透视或正交)。 • 变化频率:极低。通常只有在窗口大小改变(ChangeSize)时才需要重新计算。 • 层级关系:通常不需要复杂的层级嵌套,它是一个全局的“滤镜”。
为什么要分开? 如果把它们混在一个栈里,当你想要旋转物体时,你不得不小心地不去破坏投影参数;或者当你调整窗口大小时,你需要确保不影响到物体的相对位置逻辑。分开存储使得代码逻辑更清晰,维护更安全。
性能优化与更新策略 • 投影矩阵一旦设置好(在 ChangeSize 中),在程序运行期间几乎是不变的常量。 • 模型视图矩阵每帧都在变。 • 通过分离,GPU 驱动或底层库可以更高效地处理 Uniform 变量的更新。在 RenderScene 中,我们只需要关注 MV (模型视图)矩阵的压栈和出栈,而不必每次都重新加载投影矩阵(尽管在现代 GPU 上这点开销很小,但逻辑上是分开的)。
配合 GLGeometryTransform 工作 注意代码中的这一行:
transformPipeline.SetMatrixStacks(modelViewMatrix, projectionMatrix);
GLGeometryTransform 是一个辅助类,它的作用就是自动将这两个栈顶的矩阵相乘: 它充当了“连接器”的角色。如果你只有一个栈,你就失去了这种自动组合不同性质矩阵的便利性,必须手动管理哪个部分是投影,哪个部分是视图。
为什么也需要2个GLFrame对象?
这里使用两个 GLFrame 对象(cameraFrame 和 objectFrame)是为了解耦“观察者”与“被观察物体”的变换逻辑。 在 3D 图形学中,虽然数学上“移动相机”和“反向移动整个世界”是等价的,但在代码逻辑和交互设计上,将它们分开管理有极大的优势:
职责分离:谁在动? • cameraFrame (观察者/相机): • 职责:代表**你(用户)**在虚拟世界中的位置和视角。 • 行为:通常由场景初始化决定(如后退 15 米以便看到物体),或者由鼠标/键盘控制漫游(Walk/Fly)。 • 在本例中:它在 SetupRC 中被固定设置 MoveForward(-15.0f),意味着相机静止在远处看着原点。 • objectFrame (被观察物体): • 职责:代表场景中的模型(球体、圆环等)的位置和姿态。 • 行为:通常由动画、物理引擎或用户交互(如旋转模型)决定。 • 在本例中:它响应用户的方向键输入 (SpecialKeys),执行 RotateWorld,让物体在原地自转。
交互逻辑的独立性 想象一下,如果你只有一个 Frame: • 当你按“上键”时,你是希望相机向前飞,还是希望物体向上翻转? • 如果混在一起,你需要编写复杂的逻辑来判断当前处于“相机模式”还是“物体编辑模式”。 通过拥有两个独立的 Frame: • 你可以轻松地实现:相机不动,物体旋转(当前代码的效果)。 • 也可以轻松扩展为:物体不动,相机围绕物体旋转(轨道相机)。 • 甚至可以:两者都动(比如你在追逐一个滚动的球)。
矩阵构建的清晰流程 在 RenderScene 中,这种分离使得矩阵乘法的过程非常直观:
// 1. 获取相机的变换矩阵 (View Matrix)
cameraFrame.GetCameraMatrix(mCamera);
modelViewMatrix.MultMatrix(mCamera);
// 2. 获取物体的变换矩阵 (Model Matrix)
objectFrame.GetMatrix(mObjectFrame);
modelViewMatrix.MultMatrix(mObjectFrame);
最终的模型视图矩阵 实际上是 (视图矩阵*模型矩阵)。 • 来自 cameraFrame。 • 来自 objectFrame。







