欢迎光临
我们一直在努力

OpenGL(六)- 基础管线渲染实现

前言

上篇文章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;
}

}

效果预览: 1

这里看下代码如何实现的。

首先看下最前面定义的一些全局变量:

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(模型视图投影)矩阵,和颜色值,然后进入此前介绍的渲染流程。

最后交换缓冲区上屏显示。

这个过程就像是在组装一个变换链条:

  • PushMatrix: 记下起点。
  • MultMatrix(Camera): 加上“眼睛”的位置。 • 当前矩阵 = View
  • MultMatrix(Object): 加上“物体”的姿势。 • 当前矩阵 = View × Model
  • Draw: 结合投影矩阵 (Projection),变成 Proj × View × Model,发送给 GPU 绘图。
  • PopMatrix (在代码末尾): 回到起点,清除刚才加的 Camera 和 Object 变换,准备下一帧或下一个物体。
  • 简单来说:这段代码就是在告诉 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);

    }

    这个接口用于同时显示物体的填充颜色和黑色轮廓线:

  • 绘制填充:使用绿色 (vGreen) 正常绘制多边形填充。
  • 启用偏移:glPolygonOffset(-1.0f, -1.0f) 和 GL_POLYGON_OFFSET_FILL。这会让后续绘制的线条在深度缓冲中稍微“浮”在填充面上方,防止 Z-fighting(闪烁)。
  • 切换为线框模式:glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)。此时 OpenGL 不再填充三角形内部,只画边线。
  • 绘制轮廓:使用黑色 (vBlack) 再次绘制同一个 Batch。
  • 恢复状态:改回 GL_FILL,禁用偏移和混合,恢复线宽。
  • 思考

    为什么需要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 是一个辅助类,它的作用就是自动将这两个栈顶的矩阵相乘: 它充当了“连接器”的角色。如果你只有一个栈,你就失去了这种自动组合不同性质矩阵的便利性,必须手动管理哪个部分是投影,哪个部分是视图。

  • 历史遗留与习惯(从固定管线演变而来) 在旧版 OpenGL(固定管线)中,有 GL_MODELVIEW 和 GL_PROJECTION 两种明确的矩阵模式 (glMatrixMode)。 虽然现代 OpenGL (Core Profile) 废除了这些全局状态,要求开发者自己管理矩阵,但逻辑上的分类依然保留了下来。GLTools 库设计这两个栈正是为了迎合这种标准的图形学思维模式,让从旧版过渡来的开发者或初学者更容易理解。 总结 • modelViewMatrix:管**“位置和角度”(动得勤,层级多)。 • projectionMatrix:管“镜头和视角”**(动得少,全局统一)。 • 分开的好处:逻辑清晰、互不干扰、便于单独更新、符合图形学标准流程。 如果你强行只用一个栈,你也能实现功能,但代码会变得混乱,尤其是在处理复杂场景(多个物体、多个相机或多个视口)时,分离管理是最佳实践。
  • 为什么也需要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。

  • 类比理解 • cameraFrame 就像是你手中的摄像机。你可以拿着它到处走,或者站在原地不动。 • objectFrame 就像是舞台上的演员。演员可以在舞台上跳舞、旋转,而不管摄像机在哪里。 总结 使用两个 GLFrame 是为了模块化。 • 如果你想改变观察角度,只修改 cameraFrame。 • 如果你想让物体动起来,只修改 objectFrame。 这种设计模式是几乎所有 3D 引擎(如 Unity, Unreal, OpenGL 示例框架)的标准做法。
  • 赞(0)
    未经允许不得转载:171主机测评 » OpenGL(六)- 基础管线渲染实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址