Scene Graph 必须用递归更新变换矩阵,因为每个节点仅存储本地变换(localTransform),其世界变换(worldTransform)需由父节点 worldTransform × 自身 localTransform 逐级计算得出;根节点 worldTransform 等于 localTransform,子节点依赖父节点结果,不递归则导致坐标漂移、动画错乱。

Scene Graph 的核心设计:为什么必须用递归更新变换矩阵
游戏场景图不是简单地把对象塞进容器,而是要让每个节点的 transform(位置/旋转/缩放)自动继承父节点变化。如果不递归更新,子节点会“漂移”——比如角色挥剑时,剑模型仍固定在世界原点,而不是绕角色右肩旋转。
关键在于:每个节点只存本地变换(localTransform),世界变换(worldTransform)必须由父节点的 worldTransform × 自身 localTransform 得到。根节点的 worldTransform 就是其 localTransform。
- 用
std::vector<:shared_ptr>></:shared_ptr>存子节点,避免裸指针生命周期失控 - 父节点指针用
weak_ptr(std::weak_ptr<node></node>),防止循环引用导致内存泄漏 - 不要在每帧重算所有节点的世界矩阵——只标记脏(
dirty)节点,从根开始向上标记、向下更新
如何正确实现 UpdateWorldTransform() 递归逻辑
错误写法是让每个节点自己调用 UpdateWorldTransform(),结果可能重复计算或漏更新。正确做法是统一由根节点触发,自顶向下传播。
示例关键片段:
void Node::UpdateWorldTransform() {
if (parent.lock()) {
worldTransform = parent.lock()->worldTransform * localTransform;
} else {
worldTransform = localTransform;
}
// 标记自身为已更新
dirty = false;
// 递归更新所有子节点
for (auto& child : children) {
child->UpdateWorldTransform();
}
}
- 必须检查
parent.lock()是否为空,否则解引用空weak_ptr会崩溃 - 矩阵乘法顺序不能颠倒:
parent.world * local才符合 OpenGL/DirectX 的列主序约定;若用行主序库(如 glm::row_major),需确认glm::mat4默认行为 - 如果节点被多次添加到不同父节点(常见于 UI 复用),需在
SetParent()中先从旧父节点移除,再加入新父节点
性能陷阱:什么时候该跳过更新?
每帧都递归更新全部节点,在 1000+ 节点的场景里会成为 CPU 瓶颈。真正需要更新的只有:刚创建、刚移动、刚旋转、刚缩放、或父链上有变更的节点。
- 给每个节点加
bool dirty成员,默认false;调用SetPosition()等修改本地变换的函数时,设为true - 根节点的
Update()先做一次“脏标记传播”:从叶子往根回溯,只要子节点dirty,就置父节点dirty = true - 再自顶向下更新:仅当
dirty == true时才执行矩阵乘法并清dirty,否则直接跳过 - 注意:摄像机节点也必须参与此流程,否则视锥剔除坐标系错乱
与渲染管线对接:为什么不能直接传 worldTransform 给 shader
很多初学者把每个节点的 worldTransform 塞进 uniform buffer 逐个绘制,结果发现 GPU 绑定开销爆炸。实际中,绝大多数静态物体(地形、建筑)根本不需要 per-node world 变换——它们应合批(batch)进同一 draw call,用 instance data + base matrix 实现。
- 动态物体(角色、特效)才需单独上传
worldTransform,且应按材质/Shader 状态分组,减少 state change - 如果使用骨骼动画,
worldTransform是蒙皮前的 bone transform,此时需额外维护inverseBindPose,和 scene graph 的 world transform 分属两套系统,别混用 - 调试时可临时在
Node中加std::string name,打印name + " world: " + worldTransform[3][0]...快速定位某节点是否卡在局部坐标系没更新
父子层级的数学约束很硬,但实现上最易出问题的是生命周期管理与脏标记时机——这两个地方一旦出错,表现往往是“有时动有时不动”,比崩溃更难排查。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











