
中级OpenGL教程 022探秘三维世界的血脉传承——物体父子关系与矩阵递归奥义 序章三维世界的伦理纲常 Bilibili 同步视频 第一章缘起Blender——直观感知父子之缘 1.1 初遇两物独立各不相干 1.2 结缘CtrlP父子定盟⚡ 1.3 神效父动子随子变父安 1.4 繁衍层级嵌套生生不息️ 第二章数据筑基——Object类父子结构的匠心设计 2.1 设计理念双向指针各得其所 2.2 成员变量两大支柱撑起层级⚙️ 2.3 成员函数三大接口各司其职 2.4 addChild详解三步成礼慎终如始 第一步查重——防重复避混乱➕ 第二步入列——添新丁入族谱 第三步认父——告所生明归属 2.5 简单函数获取父子直截了当⚡ 第三章矩阵分野——本地与世界的两重天地 3.1 矩阵之要义变换之载体 3.2 两种矩阵本地与世界 本地矩阵Local Matrix 世界矩阵World Matrix 3.3 层级示例祖孙三代的矩阵推演 第四章递归妙法——层级矩阵的链式推演 4.1 递归思想自己调用自己 4.2 递归三要素 4.3 递归过程推演祖孙三代的故事第一幕Object2登场第二幕Object1接力第三幕Object0——递归出口第四幕Object1收结果第五幕Object2终成正果 第五章代码实战——从理论到工程的落地之路 ⚒️️ 5.1 改造getModelMatrix函数 5.2 测试场景搭建场景设计 5.3 预期效果验证 第六章性能考量与进阶思考 ⚡ 6.1 性能优化缓存机制 6.2 场景图Scene Graph 6.3 进阶话题预告 结语道阻且长行则将至 序章三维世界的伦理纲常 盖闻天地万物皆有伦常父子相继代代相传。 三维图形之世界亦复如是。夫物体者非孤立之存在实有层级之关联。父动而子随子变而父安此乃自然之理也。今有OpenGL图形编程之学欲通模型变换之玄妙必先明父子关系之精义。 本篇博文将自Blender直观之象始至C代码实现终层层递进步步深入冀诸君读后于层级变换之理豁然开朗焉。Bilibili 同步视频中级OpenGL教程 022探秘三维世界的血脉传承——物体父子关系与矩阵递归奥义 第一章缘起Blender——直观感知父子之缘 1.1 初遇两物独立各不相干昔者Blender之中有立方体Cube与球体Sphere二物并存于场景之内。️ 方其未结父子之缘也立方体移则立方体自移球体能转则球体自转二者泾渭分明毫无干涉。譬之如世间陌路之人擦肩而过互不相识。你走你的阳关道我过我的独木桥。♂️♀️ 平移也旋转也缩放也皆各行其是不相为谋。** 核心观察**未建立父子关系时物体之间相互独立变换互不影响。 1.2 结缘CtrlP父子定盟然则如何使二物结为父子血脉相连其法至简先选球体子再选立方体父按下CtrlP择保持变换则父子之盟定矣。结盟之后但见绿线一条牵于二物之间此Blender示父子关系之特有表达也。 再观场景集合之面板立方体之下球体在焉俨然父在上而子在下秩序井然。⚡ 1.3 神效父动子随子变父安父子关系既成则奇妙之象生焉 **父移子随**移动立方体则球体随之而动如影随形 **父转子随**旋转立方体则球体绕父而转不离不弃 **父缩子随**缩放立方体则球体同步伸缩比例不失 **子变父安**球体可单独平移、旋转、缩放而立方体岿然不动。嗟夫父之变换无不加于子子之变换不及于父。此乃父子关系之金科玉律也。⚖️ 1.4 繁衍层级嵌套生生不息父子之关系非止于一级也。子又有子孙又有孙子子孙孙无穷匮也。 一层之上更有一层一层之下复有一层三层四层五层皆可随意构建。譬如大树之有枝干干生枝枝生叶层层相属蔚为大观。此即三维场景之**层级树Hierarchy Tree**是也。️ 第二章数据筑基——Object类父子结构的匠心设计 2.1 设计理念双向指针各得其所既明父子关系之理则当思如何以代码实现之。 吾辈程序员遇问题必先思数据结构。父子关系者树形结构也其实现之法在于双向记录 **父知其子**每个父物体须有一子物体列表知晓膝下诸子 **子知其父**每个子物体须有一父物体指针知晓所从何来。** 重要原则**一父可有众子一子唯有一父。若一子而多父则听谁之命逻辑大乱矣。故常规设计之下单父为正理。 2.2 成员变量两大支柱撑起层级于Object类之中当添二成员变量// 头文件Object.h#includevector// 引入vector容器#includestring// 引入字符串#includemap// 引入映射表classObject{private:// ────────────── 父子关系核心数据 ──────────────std::vectorObject*m_children;// 子物体数组存储所有子对象指针Object*m_parent;// 父物体指针指向唯一的父对象// ... 其他成员位置、旋转、缩放等 ...public:// ... 其他函数声明 ...};m_children者vector容器也其中存放子物体之指针数量不限多子多福。m_parent者指针也指向其父物体。初始化为nullptr盖因顶层物体无父如孙悟空之石破天惊无父无母。⚙️ 2.3 成员函数三大接口各司其职数据既备函数随之。当有三函数以操作父子关系函数名功用addChild()添加子物体将新子纳入膝下并告知其谁为父getChildren()获取子物体列表返回所有子物体的拷贝getParent()获取父物体返回父物体指针 2.4 addChild详解三步成礼慎终如始addChild函数看似简单实则暗藏玄机。其执行有三步一步不可缺 第一步查重——防重复避混乱加入之前必先检查此子是否已在膝下若已在而又加则一而二二而三数组之中重复狼藉后患无穷。查重之法用STL之find函数遍历vector寻觅目标。若得之则打印错误提前返回不复执行后续。➕ 第二步入列——添新丁入族谱若查重通过无重复之虞则将子物体指针push_back入m_children数组之中。此一步最为简单然亦最为基础。 第三步认父——告所生明归属**此步最为关键万万不可忘却**⚠️ 子物体既入父之数组父知有子矣然子尚不知其父为谁岂不悲哉故必设子物体之m_parent为this指针令子知晓“吾之父即此对象也。” 如此双向贯通父子关系方得圆满。✨// 源文件Object.cpp#includealgorithm// 引入std::find#includeiostream// 引入错误输出/** * brief 添加子物体 * param obj 待添加的子物体指针 * * 三步曲 * 1. 查重避免重复添加同一子物体 * 2. 入列将子物体加入children数组 * 3. 认父设置子物体的parent指针关键 */voidObject::addChild(Object*obj){// ── 第一步查重 ──// 使用std::find在vector中查找目标对象autoiterstd::find(m_children.begin(),// 起始迭代器m_children.end(),// 结束迭代器obj// 要查找的目标);// 若迭代器不等于end说明找到了即已存在if(iter!m_children.end()){std::cerr❌ 错误重复添加子物体std::endl;return;// 提前返回不执行后续}// ── 第二步入列 ──m_children.push_back(obj);// 将子物体加入数组// ── 第三步认父重中之重──obj-m_parentthis;// 告诉子物体我是你爸爸}** 迭代器小知识**std::find返回迭代器iterator其性类似指针。若找到目标迭代器指向该元素若未找到则指向容器末尾end()。end()并非最后一个元素而是最后一个元素之后的空位置。 2.5 简单函数获取父子直截了当getChildren与getParent二函数最为简单直接返回对应成员变量即可。/** * brief 获取所有子物体返回拷贝防止外部修改内部数据 * return 子物体数组的副本 */std::vectorObject*Object::getChildren(){returnm_children;// 返回拷贝保护内部数据}/** * brief 获取父物体指针 * return 父物体指针若无父则返回nullptr */Object*Object::getParent(){returnm_parent;// 直接返回父指针}**设计考量**getChildren返回拷贝而非引用何也盖因若返回引用则外部代码可随意修改内部数组破坏封装性。返回拷贝虽有少许性能开销然安全性大增权衡之下此为上策。️⚡ 第三章矩阵分野——本地与世界的两重天地 3.1 矩阵之要义变换之载体夫三维图形者矩阵之天下也。 平移、旋转、缩放无一不可以矩阵表之。每一物体皆有其Model Matrix模型矩阵决定其在空间中之位置、朝向与大小。昔者未涉父子关系之时getModelMatrix函数但凭自身之position、rotation、scale三参数计算变换矩阵。此矩阵仅关乎自身不及其他。 3.2 两种矩阵本地与世界父子关系既入则矩阵之分不可不察。矩阵有二 本地矩阵Local Matrix本地矩阵者物体相对于其父之变换也。 仅用自身之位置、旋转、缩放计算而得不考虑父物体之影响。譬之如人在家中相对于客厅沙发之位置此即本地坐标也。你在沙发左三尺右二尺皆相对于沙发而言与房屋在城市中之位置无涉。 世界矩阵World Matrix世界矩阵者物体在整个场景中之最终变换也。 须将自身本地矩阵与父之世界矩阵祖父之世界矩阵层层相乘方得最终结果。譬之如人在地球上之经纬度须将家中位置、“城市位置”、国家位置层层叠加方得最终之全球坐标。 核心公式世界矩阵 本地矩阵 × 父物体世界矩阵注意矩阵乘法顺序至关重要不可颠倒 3.3 层级示例祖孙三代的矩阵推演试举一例以明其理。假设有三物体祖孙三代Object0祖父→ Object1父→ Object2子。各物体之世界矩阵当如何计算 **Object0顶层**无父无母石头里蹦出来的。其世界矩阵 本地矩阵。盖因无人影响之自身即世界也。 **Object1中层**有父Object0。其世界矩阵 Object1本地矩阵 × Object0世界矩阵。 **Object2底层**有父Object1。其世界矩阵 Object2本地矩阵 × Object1世界矩阵。展开观之Object2世界矩阵 T2 × T1 × T0 自身本地 × 父世界 × 祖世界 由近及远层层相乘其中T0、T1、T2分别为Object0、Object1、Object2之本地变换矩阵。 第四章递归妙法——层级矩阵的链式推演 4.1 递归思想自己调用自己然则如何实现此层层相乘之逻辑答曰递归。递归者何函数自己调用自己之谓也。欲算我之世界矩阵必先算父之世界矩阵欲算父之世界矩阵必先算祖之世界矩阵……直至顶层无父而止。此过程如剥洋葱层层深入直至核心又如登塔自下而上逐级攀升。 4.2 递归三要素要素说明递归出口父物体为nullptr时停止递归返回单位矩阵或自身本地矩阵递归调用有父物体时调用父物体的getModelMatrix()获取父之世界矩阵合并结果将自身本地矩阵与父之世界矩阵相乘得到自身世界矩阵并返回 4.3 递归过程推演祖孙三代的故事设吾辈调用Object2之getModelMatrix()则递归过程如下诸君且细品之第一幕Object2登场Object2曰“欲算我之世界矩阵须知我父之世界矩阵。” 遂调用Object1-getModelMatrix()。自己则暂停等待如子待父命。⏸️第二幕Object1接力Object1曰“欲算我之世界矩阵须知我父之世界矩阵。” 遂调用Object0-getModelMatrix()。自己亦暂停等待如父待祖命。⏸️第三幕Object0——递归出口Object0曰“我无父也我的本地矩阵即我的世界矩阵。” 遂计算自身本地矩阵直接返回。 此即递归之出口触底反弹之时也。第四幕Object1收结果Object1得Object0返回之矩阵大喜。遂以自身本地矩阵乘之得自身世界矩阵返回给Object2。第五幕Object2终成正果Object2得Object1返回之矩阵亦大喜。遂以自身本地矩阵乘之得自身世界矩阵。 至此递归完成结果得矣。** 递归之妙**如投石入水波纹层层扩散直至岸边出口而后折返层层归位。每一层只关心自己与上一层的关系不必知晓全局之全貌。此即递归之分而治之思想也。 第五章代码实战——从理论到工程的落地之路 ⚒️️ 5.1 改造getModelMatrix函数理论既明代码随之。改造getModelMatrix函数加入递归逻辑其法有三/** * brief 获取模型的世界变换矩阵递归版本 * return 4x4世界变换矩阵 * * 核心逻辑 * 1. 获取父物体的世界矩阵递归调用 * 2. 计算自身的本地变换矩阵 * 3. 本地矩阵 × 父世界矩阵 自身世界矩阵 */mat4Object::getModelMatrix(){// ────────── 第一步获取父物体的世界矩阵 ──────────mat4 parentMatrixmat4(1.0f);// 初始化为单位矩阵// 若有父物体则递归获取父之世界矩阵if(m_parent!nullptr){parentMatrixm_parent-getModelMatrix();// 递归调用}// 若无父物体parentMatrix保持单位矩阵乘之不影响结果// ────────── 第二步计算自身本地变换矩阵 ──────────mat4 scaleMatrix...;// 缩放矩阵根据m_scale计算mat4 rotateMatrix...;// 旋转矩阵根据m_rotation计算mat4 translateMatrix...;// 平移矩阵根据m_position计算// 组合本地变换平移 × 旋转 × 缩放mat4 localMatrixtranslateMatrix*rotateMatrix*scaleMatrix;// ────────── 第三步本地矩阵 × 父世界矩阵 ──────────// 注意乘法顺序父矩阵在右自身在左mat4 worldMatrixlocalMatrix*parentMatrix;returnworldMatrix;}** 矩阵乘法顺序警告**矩阵乘法不满足交换律顺序至关重要**正确**自身本地矩阵 × 父世界矩阵local × parent**错误**父世界矩阵 × 自身本地矩阵parent × local顺序颠倒则变换效果完全不同切记切记 5.2 测试场景搭建代码既成当测试之。测试之法当建一场景观其效果。场景设计 **立方体父**位于原点绕Z轴旋转每帧转4度⚽ **球体子**位于X2处为立方体之子自身绕Y轴旋转每帧转6度预期效果球体既随立方体绕Z轴公转又绕自身Y轴自转如行星之绕日同时自转。☀️// 在场景初始化函数中voidprepareScene(){// ── 1. 创建立方体父物体──autocubeGeometryGeometry::createCube(1.0f);autocubeMeshnewMesh(cubeGeometry);cubeMesh-setPosition(vec3(0.0f,0.0f,0.0f));// 位于原点// ── 2. 创建球体子物体──autosphereGeometryGeometry::createSphere(1.0f);autosphereMeshnewMesh(sphereGeometry);sphereMesh-setPosition(vec3(2.0f,0.0f,0.0f));// X2处// ── 3. 建立父子关系关键一步──cubeMesh-addChild(sphereMesh);// 球体成为立方体的子物体// ── 4. 加入场景渲染列表──meshes.push_back(cubeMesh);meshes.push_back(sphereMesh);}// 在每帧更新函数中voidupdateScene(floatdeltaTime){// 父物体立方体绕Z轴旋转公转meshes[0]-rotateAroundZ(4.0f*deltaTime);// 子物体球体绕自身Y轴旋转自转meshes[1]-rotateAroundY(6.0f*deltaTime);} 5.3 预期效果验证运行程序当见何景象 立方体绕Z轴缓缓旋转如日之经天 球体随立方体同步旋转如行星之绕日公转⚡ 球体同时绕自身Y轴快速旋转如地球之自转 球体始终保持与立方体的相对距离X2不离不弃。若效果如此则父子关系之矩阵计算大功告成矣 第六章性能考量与进阶思考 ⚡ 6.1 性能优化缓存机制递归虽妙然亦有弊。若每帧皆从头递归计算层级深时重复计算甚多性能堪忧。优化之法在于缓存Cache物体计算过世界矩阵后存之若父物体未变自身亦未变则直接返回缓存之矩阵不必重算。classObject{private:mat4 m_cachedWorldMatrix;// 缓存的世界矩阵boolm_isDirty;// 脏标记是否需要重新计算// ... 其他成员 ...public:// 当位置/旋转/缩放改变时标记为脏voidsetPosition(vec3 pos){m_positionpos;markDirty();// 标记自己和所有子物体为脏}// 标记脏自己变了孩子们也得重新算voidmarkDirty(){m_isDirtytrue;for(autochild:m_children){child-markDirty();// 递归标记子物体}}}; 6.2 场景图Scene Graph父子关系之终极形态即**场景图Scene Graph**是也。️ 整个场景为一树形结构根节点之下众子分焉子又有子层层嵌套。渲染之时自根节点始深度优先遍历依次计算各节点之世界矩阵而后渲染。此即现代游戏引擎之标准做法也。 6.3 进阶话题预告本篇所讲乃父子关系之基础。进阶之境尚有诸多玄妙 **变换空间转换**本地空间→世界空间→观察空间→裁剪空间空间转换之链条 **反向动力学IK**子动而父随如人伸手而臂随之骨骼动画之核心 **骨骼蒙皮动画**骨骼层级驱动顶点形变角色动画之基石 **四元数与万向锁**旋转之更深奥义避免欧拉角之弊端。此皆后话容后再表。 结语道阻且长行则将至 呜呼父子关系看似简单实则蕴含图形学之至理。 数据结构之设计矩阵乘法之顺序递归思想之运用无一不考验程序员之功底。然诸君不必畏难。天下难事必作于易天下大事必作于细。 自Blender直观之象入手而数据结构而矩阵原理而递归实现步步为营久久为功则终有豁然贯通之一日。愿诸君以此篇为阶登堂入室于三维图形之广袤天地纵横驰骋游刃有余。 路漫漫其修远兮吾将上下而求索。与诸君共勉 本篇要点回顾✅ 父子关系父动子随子变父安单向传递✅ 数据结构children数组 parent指针双向记录✅ addChild三步查重、入列、认父一步不可少✅ 两种矩阵本地矩阵自身vs 世界矩阵最终✅ 递归计算世界矩阵 本地矩阵 × 父世界矩阵✅ 乘法顺序矩阵不可交换顺序至关重要✅ 性能优化脏标记 缓存避免重复计算— 全文完 — 感谢阅读如有疑问欢迎评论区交流