中级OpenGL教程 010:Object 类设计与模型矩阵完全实现

中级OpenGL教程 010:Object 类设计与模型矩阵完全实现

  • [Bilibili 同步视频](#Bilibili 同步视频)
  • [一、Object 类:3D 物体的「空间管理者」](#一、Object 类:3D 物体的「空间管理者」)
    • [1.1 文件结构与基础配置](#1.1 文件结构与基础配置)
  • [二、成员变量设计:protected 权限的精妙选择](#二、成员变量设计:protected 权限的精妙选择)
    • [2.1 三大核心变换变量](#2.1 三大核心变换变量)
  • [三、旋转逻辑:本地坐标系 + Unity 标准顺序](#三、旋转逻辑:本地坐标系 + Unity 标准顺序)
    • [3.1 本地坐标系旋转](#3.1 本地坐标系旋转)
    • [3.2 旋转顺序:严格遵循 Unity 引擎标准](#3.2 旋转顺序:严格遵循 Unity 引擎标准)
  • [四、接口封装:简洁易用的变换 API](#四、接口封装:简洁易用的变换 API)
    • [4.1 核心接口声明](#4.1 核心接口声明)
    • [4.2 接口实现(object.cpp)](#4.2 接口实现(object.cpp))
  • 五、模型矩阵计算:渲染管线的「核心输出」
    • [5.1 矩阵计算完整实现](#5.1 矩阵计算完整实现)
    • [5.2 关键细节勘误(必看)](#5.2 关键细节勘误(必看))
  • [六、Object 类的价值与扩展方向](#六、Object 类的价值与扩展方向)
    • [6.1 核心价值](#6.1 核心价值)
    • [6.2 扩展方向](#6.2 扩展方向)
  • 结语

Bilibili 同步视频

中级OpenGL教程 010:Object 类设计与模型矩阵完全实现

在 3D 图形渲染的世界里,一切可见元素都可被抽象为「物体」------ 无论是场景中的模型、角色、道具,还是粒子特效,都需要一套统一的框架来管理其空间变换。Object 类 正是这套框架的核心,它封装了物体的位移、旋转、缩放三大基础变换,最终输出渲染管线必需的模型矩阵(Model Matrix),为后续 MVP 矩阵计算与片元着色打下坚实基础。

本文将从零拆解 Object 类的设计思路、成员变量定义、接口封装与模型矩阵计算逻辑,搭配可直接落地的 C++ 代码实现,帮你快速搭建 3D 引擎的物体管理模块✨。


一、Object 类:3D 物体的「空间管理者」

Object 类的核心使命只有一个:管理物体的空间变换,输出标准模型矩阵

它是所有可渲染物体的基类,场景中的模型、相机、灯光等实体,都可继承此类复用变换逻辑。因此在设计时,权限控制、变量封装、接口易用性是三大关键原则。

1.1 文件结构与基础配置

我们将 Object 类置于 GL framework 核心模块,方便全工程复用,创建两个核心文件:

  • object.h:类定义、变量声明、接口声明

  • object.cpp:函数实现、矩阵计算逻辑

头文件必须添加 #pragma once 预编译指令,杜绝头文件二次编译,避免重复定义报错;同时引入核心依赖库(GLM 数学库、框架核心文件),为矩阵运算、向量操作提供支撑。

cpp 复制代码
// object.h 基础配置
#pragma once
#include "core.h" // 包含 GLM 数学库、框架核心工具

二、成员变量设计:protected 权限的精妙选择

物体的空间变换,本质是位置、旋转、缩放 三组数据的组合。

在权限设计上,我们放弃 private,选择 protected 修饰所有成员变量 ------ 这是为了让子类能直接访问父类变换数据,保证继承体系的灵活性(比如模型类、角色类可直接修改位置、旋转角度)。

2.1 三大核心变换变量

  1. 位置变量:m_position

    记录物体在世界坐标系中的坐标,用 glm::vec3 存储,初始化为 (0.0f, 0.0f, 0.0f),代表物体默认位于世界原点。

  2. 旋转变量:m_angle_x /m_angle_y/m_angle_z

    采用欧拉角旋转方案,分别记录物体绕 X、Y、Z 本地轴的旋转角度,这是游戏 / 引擎中最通用的旋转方式:

    • Pitch(绕 X 轴):上下俯仰(类比「点头」)

    • Yaw(绕 Y 轴):左右转向(类比「摇头」)

    • Roll(绕 Z 轴):滚筒旋转(类比「滚床单」)

  3. 缩放变量:m_scale

    glm::vec3 存储物体在 X、Y、Z 轴的缩放比例,默认值必须为 1.0f(代表无缩放),若设为 0 会导致物体完全消失。

cpp 复制代码
// object.h 成员变量定义
class Object {
protected:
    // 1. 位置:世界坐标系坐标
    glm::vec3 m_position{ 0.0f, 0.0f, 0.0f };
    // 2. 旋转:欧拉角(绕本地X/Y/Z轴)
    float m_angle_x{ 0.0f };
    float m_angle_y{ 0.0f };
    float m_angle_z{ 0.0f };
    // 3. 缩放:三轴缩放比例
    glm::vec3 m_scale{ 1.0f, 1.0f, 1.0f };

public:
    // 后续接口声明...
};

三、旋转逻辑:本地坐标系 + Unity 标准顺序

旋转是 3D 变换中最易出错的环节,核心要解决两个问题:基于什么坐标系旋转?旋转的先后顺序是什么?

3.1 本地坐标系旋转

我们采用本地坐标系(局部坐标系) 旋转规则:

物体的旋转始终围绕自身当前的坐标轴 ,而非世界坐标系坐标轴。

比如物体先绕 X 轴旋转 30°,再绕 Z 轴旋转时,Z 轴是物体旋转后的本地 Z 轴,而非世界 Z 轴 ------ 这符合现实中物体的运动直觉(如飞机、汽车的运动)。

3.2 旋转顺序:严格遵循 Unity 引擎标准

为保证旋转结果可预期,固定旋转顺序 至关重要,我们直接沿用工业级标准(Unity 引擎):

先 Pitch(X 轴)→ 再 Yaw(Y 轴)→ 最后 Roll(Z 轴)

此顺序能最大程度避免万向锁问题,适配绝大多数 3D 场景需求。


四、接口封装:简洁易用的变换 API

Object 类对外提供极简接口,支持设置位置、增量旋转、设置缩放三大操作,区分「赋值式设置」与「增量式修改」,符合引擎开发习惯。

4.1 核心接口声明

cpp 复制代码
// object.h 公共接口
public:
    Object() = default;
    ~Object() = default;

    // 1. 设置位置:赋值式(直接覆盖世界坐标)
    void setPosition(const glm::vec3& position);
    // 2. 增量旋转:在原有角度基础上累加
    void rotateX(float angle);
    void rotateY(float angle);
    void rotateZ(float angle);
    // 3. 设置缩放:赋值式(直接覆盖三轴缩放)
    void setScale(const glm::vec3& scale);
    // 4. 核心:计算并返回模型矩阵
    glm::mat4 getModelMatrix();

4.2 接口实现(object.cpp)

旋转接口采用增量累加 逻辑,每调用一次 rotateX,就在原有角度上叠加新角度,适配帧更新的旋转逻辑;位置与缩放为直接赋值,保证精准控制。

cpp 复制代码
// object.cpp 接口实现
#include "object.h"

// 设置位置
void Object::setPosition(const glm::vec3& position) {
    m_position = position;
}

// 增量旋转 X 轴
void Object::rotateX(float angle) {
    m_angle_x += angle;
}
// 增量旋转 Y 轴
void Object::rotateY(float angle) {
    m_angle_y += angle;
}
// 增量旋转 Z 轴
void Object::rotateZ(float angle) {
    m_angle_z += angle;
}

// 设置缩放
void Object::setScale(const glm::vec3& scale) {
    m_scale = scale;
}

五、模型矩阵计算:渲染管线的「核心输出」

getModelMatrix() 是 Object 类的灵魂函数,它将位移、旋转、缩放三组离散数据,组合为渲染管线必需的模型矩阵 ,变换顺序严格遵循 Unity 标准:

先缩放 → 再旋转 → 最后平移

⚠️ 关键规则:平移必须在最后,且基于世界坐标系执行,保证物体最终定位到指定世界坐标。

5.1 矩阵计算完整实现

cpp 复制代码
// object.cpp 模型矩阵计算
glm::mat4 Object::getModelMatrix() {
    // 1. 初始化单位矩阵
    glm::mat4 transform = glm::mat4(1.0f);

    // 2. 第一步:缩放变换(本地坐标系)
    transform = glm::scale(transform, m_scale);

    // 3. 第二步:旋转变换(本地坐标系,严格按 X→Y→Z 顺序)
    // 旋转需转弧度:GLM 仅支持弧度运算,角度必须转换!
    transform = glm::rotate(transform, glm::radians(m_angle_x), glm::vec3(1.0f, 0.0f, 0.0f));
    transform = glm::rotate(transform, glm::radians(m_angle_y), glm::vec3(0.0f, 1.0f, 0.0f));
    transform = glm::rotate(transform, glm::radians(m_angle_z), glm::vec3(0.0f, 0.0f, 1.0f));

    // 4. 第三步:平移变换(世界坐标系,单独计算后相乘)
    transform = glm::translate(glm::mat4(1.0f), m_position) * transform;

    // 返回最终模型矩阵
    return transform;
}

5.2 关键细节勘误(必看)

GLM 数学库的旋转函数仅支持弧度值 ,但我们设计的 m_angle_x/y/z 是角度值,必须用 glm::radians() 转换,否则旋转结果完全错误,这是 3D 开发高频踩坑点❗


六、Object 类的价值与扩展方向

6.1 核心价值

  1. 统一抽象:将所有 3D 物体的变换逻辑收敛到一个基类,减少重复代码

  2. 标准输出:输出合规模型矩阵,无缝对接渲染管线

  3. 易于继承:子类可快速扩展(如 Model 类添加网格数据、Light 类添加光照参数)

6.2 扩展方向

  • 新增 resetTransform() 重置所有变换

  • 添加欧拉角转四元数,解决万向锁问题

  • 支持父物体 / 子物体的父子坐标系变换


结语

Object 类是 3D 渲染框架的「最小可用单元」,它没有复杂的逻辑,却承载了物体空间变换的核心能力。从变量权限设计、旋转规则制定,到模型矩阵计算,每一步都遵循工业级引擎标准,代码简洁、可直接落地到 OpenGL/ES 3D 项目中。

掌握 Object 类,你就掌握了 3D 场景中物体运动的底层逻辑,后续再扩展相机、光照、材质等模块,都会变得水到渠成🌊。