VulkanSceneGraph学习教程(十四) 第 14 章 变换与动画本章定位静态场景有了动态才有生命力。本章讲MatrixTransform变换与 VSG 的现代动画系统——Animation 各类Sampler⚠️不是OSG 的AnimationPath。14.1 本章目标用MatrixTransform组织层级变换第 6 章已讲本章聚焦动画化理解vsg::Animation与vsg::TransformSampler的配合让一个节点沿关键帧移动/旋转知道相机/骨骼/形变动画分别对应哪个 Sampler并了解AnimationManager如何驱动更新。14.2 前置准备第 6 章MatrixTransform与第 13 章加载模型已读。14.3 ⚠️ 没有AnimationPathOSG 的osg::AnimationPath在 VSG 中不存在。VSG 的动画由一套「采样器Sampler」驱动vsg::Animation一条动画 一组AnimationSampler带播放模式ONCE / REPEAT / FORWARD_AND_BACK与速度AnimationSampler基类把「时间」映射成「对某个场景图对象的修改」具体子类TransformSampler变换、CameraSampler相机、JointSampler骨骼、MorphSampler形变。14.4Animation的结构auto animation vsg::Animation::create(); animation-name spin; animation-mode vsg::Animation::REPEAT; // 循环播放 animation-speed 1.0; // animation-samplers 是 std::vectorref_ptrAnimationSamplerAnimation::update(simulationTime)会按speed推进time再让每个sampler-update(time)去修改目标对象。14.5TransformSampler关键帧变换TransformSampler持有一个TransformKeyframes位置/旋转/缩放的关键帧序列以及object——被动画修改的目标通常是MatrixTransform// 1) 关键帧 auto keyframes vsg::TransformKeyframes::create(); keyframes-add(0.0, vsg::dvec3(0.0, 0.0, 0.0), vsg::dquat()); // t0原位 keyframes-add(2.0, vsg::dvec3(2.0, 0.0, 0.0), vsg::dquat()); // t2平移到 x2 keyframes-add(4.0, vsg::dvec3(2.0, 1.0, 0.0), vsg::dquat(0.0, 0.0, 0.707, 0.707)); // t4上移转90° // 2) 采样器绑定目标 MatrixTransform auto target vsg::MatrixTransform::create(); auto sampler vsg::TransformSampler::create(); sampler-keyframes keyframes; sampler-object target; // 每帧 apply(target) 更新其 matrix // 3) 装进 Animation auto animation vsg::Animation::create(); animation-mode vsg::Animation::REPEAT; animation-samplers.push_back(sampler);TransformSampler::transform()结果是translate(position) * rotate(rotation) * scale(scale)——与第 6 章的变换链一致。14.6 让动画跑起来AnimationManagerViewer内置一个animationManagerref_ptrAnimationManager。把动画play进去它会在viewer-update()中自动推进viewer-animationManager-play(animation); // 注册并自动随帧更新注意viewer-animationManager是Viewer的成员默认已创建无需自行new。在viewer-update()被调用时它会用当前FrameStamp调用animationManager-run(...)逐帧更新所有已播放动画。其他 Sampler 同理CameraSampler→object设为Camera/LookAt做相机飞行JointSampler→object设为Joint做骨骼动画glTF 角色MorphSampler→ 控制形变权重。14.7 完整示例让模型绕圈平移auto target vsg::MatrixTransform::create(); target-addChild(model); // model 来自第 13 章 auto keyframes vsg::TransformKeyframes::create(); for (int i 0; i 4; i) { double t i; // 每秒一帧关键帧 double a t * 0.5; // 角度 keyframes-add(t, vsg::dvec3(std::cos(a)*2.0, 0.0, std::sin(a)*2.0), vsg::dquat()); } auto sampler vsg::TransformSampler::create(); sampler-keyframes keyframes; sampler-object target; auto animation vsg::Animation::create(); animation-mode vsg::Animation::REPEAT; animation-samplers.push_back(sampler); viewer-animationManager-play(animation); // 帧循环里 viewer-update() 会自动推进下面是一个完整的、可直接运行的 C 示例包含必要的头文件、main 函数、模型加载使用简单立方体、动画创建和 Viewer 帧循环#include vsg/all.h #include iostream int main(int argc, char** argv) { // 1. 创建 Viewer 和窗口 auto options vsg::Options::create(); options-paths vsg::getEnvPaths(VSG_FILE_PATH); auto windowTraits vsg::WindowTraits::create(); windowTraits-gt;width 800; windowTraits-gt;height 600; windowTraits-gt;title VSG 动画示例 - 绕圈平移; auto viewer vsg::Viewer::create(); auto window vsg::Window::create(windowTraits); if (!window) { std::cout lt;lt; 无法创建窗口 lt;lt; std::endl; return 1; } viewer-gt;addWindow(window); // 2. 创建相机和视图 auto lookAt vsg::LookAt::create( vsg::dvec3(0.0, 5.0, 10.0), // 相机位置 vsg::dvec3(0.0, 0.0, 0.0), // 观察目标 vsg::dvec3(0.0, 1.0, 0.0) // 上方向 ); auto perspective vsg::Perspective::create( 30.0, // 垂直视场角 static_castlt;doublegt;(windowTraits-gt;width) / windowTraits-gt;height, 0.1, // 近平面 100.0 // 远平面 ); auto camera vsg::Camera::create(perspective, lookAt, vsg::ViewportState::create(window-gt;extent2D())); auto view vsg::View::create(camera); // 3. 创建场景图一个简单的彩色立方体 // 创建立方体顶点数据 auto vertices vsg::vec3Array::create({ {-0.5f, -0.5f, -0.5f}, { 0.5f, -0.5f, -0.5f}, { 0.5f, 0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f}, {-0.5f, -0.5f, 0.5f}, { 0.5f, -0.5f, 0.5f}, { 0.5f, 0.5f, 0.5f}, {-0.5f, 0.5f, 0.5f} }); auto colors vsg::vec4Array::create({ {1.0f, 0.0f, 0.0f, 1.0f}, {0.0f, 1.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f, 1.0f}, {1.0f, 1.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 1.0f, 1.0f}, {0.5f, 0.5f, 0.5f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f} }); auto indices vsg::ushortArray::create({ 0,1,2, 2,3,0, // 前 4,5,6, 6,7,4, // 后 0,4,7, 7,3,0, // 左 1,5,6, 6,2,1, // 右 3,2,6, 6,7,3, // 上 0,1,5, 5,4,0 // 下 }); auto geometry vsg::Geometry::create(); geometry-gt;arrays vsg::DataList{vertices, colors}; geometry-gt;indices indices; geometry-gt;commands.push_back(vsg::DrawIndexed::create(36, 1, 0, 0, 0)); // 4. 创建变换节点动画目标 auto target vsg::MatrixTransform::create(); target-gt;matrix vsg::translate(0.0, 0.0, 0.0); // 初始位置 target-gt;addChild(geometry); // 将立方体添加到变换节点 // 5. 创建动画关键帧绕 Y 轴做圆周运动 auto keyframes vsg::TransformKeyframes::create(); // 创建 8 个关键帧形成完整的圆周运动 for (int i 0; i lt; 8; i) { double time i * 0.5; // 每 0.5 秒一个关键帧总时长 4 秒 double angle time * (vsg::PI / 2.0); // 每秒转 90 度 // 计算圆周位置半径 3.0在 XZ 平面绕圈 double x std::cos(angle) * 3.0; double z std::sin(angle) * 3.0; // 添加关键帧位置变化旋转保持默认无旋转 keyframes-gt;add(time, vsg::dvec3(x, 0.0, z), vsg::dquat()); std::cout lt;lt; 关键帧 lt;lt; i lt;lt; : t lt;lt; time lt;lt; , 位置( lt;lt; x lt;lt; , 0.0, lt;lt; z lt;lt; ) lt;lt; std::endl; } // 6. 创建动画采样器 auto sampler vsg::TransformSampler::create(); sampler-gt;keyframes keyframes; sampler-gt;object target; // 绑定到变换节点 // 7. 创建动画 auto animation vsg::Animation::create(); animation-gt;name circle_motion; animation-gt;mode vsg::Animation::REPEAT; // 循环播放 animation-gt;speed 1.0; // 正常速度 animation-gt;samplers.push_back(sampler); // 8. 设置场景图 auto scenegraph vsg::Group::create(); scenegraph-gt;addChild(target); auto root vsg::Group::create(); root-gt;addChild(view); view-gt;addChild(scenegraph); // 9. 编译场景图 auto commandGraph vsg::createCommandGraphForView(window, camera, root); viewer-gt;assignRecordAndSubmitTaskAndPresentation({commandGraph}); // 10. 启动动画 viewer-gt;animationManager-gt;play(animation); std::cout lt;lt; 动画已启动: lt;lt; animation-gt;name lt;lt; std::endl; // 11. 主循环 viewer-gt;compile(); while (viewer-gt;advanceToNextFrame()) { // 处理窗口事件 viewer-gt;handleEvents(); // 更新动画animationManager 会自动更新 viewer-gt;update(); // 渲染帧 viewer-gt;recordAndSubmit(); viewer-gt;present(); } return 0; }关键步骤说明头文件与初始化包含vsg/all.h获取所有 VSG 功能创建 Viewer 和窗口。相机设置创建 LookAt 和 Perspective 相机确保能清楚看到动画效果。模型创建使用顶点数组和索引创建彩色立方体作为演示模型。变换节点创建MatrixTransform作为动画目标立方体作为其子节点。关键帧定义创建 8 个关键帧在 XZ 平面上形成半径为 3.0 的圆周运动。采样器绑定创建TransformSampler将关键帧绑定到变换节点。动画配置创建Animation对象设置为循环播放模式。场景图构建将变换节点添加到场景图中设置视图和相机。动画启动通过viewer-animationManager-play(animation)注册动画。主循环标准 VSG 帧循环viewer-update()会自动驱动动画更新。运行效果立方体会在 XZ 平面上以半径 3.0 做圆周运动每 4 秒完成一圈然后循环重复。控制台会输出每个关键帧的位置信息。14.7.1 动画速度与暂停控制在实际应用中我们经常需要动态控制动画的播放速度或者暂停/恢复动画。VSG 的Animation和AnimationManager提供了相应的接口来实现这些功能。1. 调整动画播放速度Animation对象的speed属性控制动画播放速度默认值为 1.0正常速度。通过修改这个值可以实现慢速如 0.5x或快速如 2.0x播放// 获取动画对象假设 animation 是之前创建的动画 auto animation ...; // 从之前代码获取 // 设置动画速度为 0.5 倍慢速播放 animation-speed 0.5; // 设置动画速度为 2.0 倍快速播放 animation-speed 2.0; // 设置动画速度为 0.0完全停止但不会暂停只是不推进时间 animation-speed 0.0; // 恢复正常速度 animation-speed 1.0;speed值可以在动画播放过程中随时修改下一帧就会生效。例如可以在键盘事件回调中动态调整// 在键盘事件处理中调整动画速度 viewer-addEventHandler(vsg::CloseHandler::create(viewer)); viewer-addEventHandler(vsg::Trackball::create(camera)); // 添加自定义键盘事件处理器 viewer-addEventHandler(vsg::KeyPressHandler::create( [animation](const vsg::KeyPressEvent keyPress) { if (keyPress.keyBase 1) { animation-speed 0.5; // 按 1 键0.5 倍速 std::cout 动画速度设置为 0.5x std::endl; } else if (keyPress.keyBase 2) { animation-speed 1.0; // 按 2 键正常速度 std::cout 动画速度设置为 1.0x std::endl; } else if (keyPress.keyBase 3) { animation-speed 2.0; // 按 3 键2.0 倍速 std::cout 动画速度设置为 2.0x std::endl; } return false; } ));2. 暂停与恢复动画虽然将speed设为 0.0 可以停止动画推进但更好的方式是使用AnimationManager的暂停/恢复功能// 暂停动画 viewer-animationManager-pause(animation); // 恢复动画 viewer-animationManager-resume(animation); // 检查动画是否正在播放 bool isPlaying viewer-animationManager-isPlaying(animation); // 停止动画从 AnimationManager 中移除 viewer-animationManager-stop(animation);下面是一个完整的示例演示如何在主循环中结合键盘事件控制动画的速度和暂停/恢复// 在之前完整示例的主循环前添加键盘事件处理器 viewer-addEventHandler(vsg::CloseHandler::create(viewer)); auto trackball vsg::Trackball::create(camera); viewer-addEventHandler(trackball); // 添加动画控制键盘处理器 viewer-addEventHandler(vsg::KeyPressHandler::create( [animation, viewer](const vsg::KeyPressEvent keyPress) { if (keyPress.keyBase 1) { // 慢速播放 animation-speed 0.5; std::cout 动画速度: 0.5x std::endl; } else if (keyPress.keyBase 2) { // 正常速度 animation-speed 1.0; std::cout 动画速度: 1.0x std::endl; } else if (keyPress.keyBase 3) { // 快速播放 animation-speed 2.0; std::cout 动画速度: 2.0x std::endl; } else if (keyPress.keyBase P || keyPress.keyBase p) { // 暂停/恢复动画 if (viewer-animationManager-isPlaying(animation)) { viewer-animationManager-pause(animation); std::cout 动画已暂停 std::endl; } else { viewer-animationManager-resume(animation); std::cout 动画已恢复 std::endl; } } else if (keyPress.keyBase S || keyPress.keyBase s) { // 停止动画 viewer-animationManager-stop(animation); std::cout 动画已停止 std::endl; } else if (keyPress.keyBase R || keyPress.keyBase r) { // 重新播放动画 viewer-animationManager-play(animation); std::cout 动画重新播放 std::endl; } return false; } )); // 主循环 while (viewer-advanceToNextFrame()) { viewer-handleEvents(); viewer-update(); viewer-recordAndSubmit(); viewer-present(); }3. 控制说明速度控制animation-speed是浮点数1.0 表示正常速度0.5 表示半速2.0 表示两倍速。值为 0.0 时动画时间不推进但动画仍处于播放状态。暂停与恢复pause()会暂停动画的时间推进但保持动画在AnimationManager中的注册状态resume()从暂停点继续播放。停止与重新播放stop()将动画从AnimationManager中移除需要重新调用play()才能再次播放。状态查询isPlaying()返回动画是否正在播放包括暂停状态。通过这些控制接口可以在运行时灵活调整动画的播放行为实现交互式的动画控制效果。14.8 常见问题现象原因解决动画不动没play进viewer-animationManager或没调用viewer-update()确认viewer-animationManager-play(...)且帧循环含update()找不到vsg::AnimationPathOSG 概念VSG 无此类型改用AnimationTransformSampler本章目标没变化sampler-object没指向实际节点把object设为场景图中真实存在的MatrixTransform跳变/不连续关键帧时间不单调或模式为 ONCE用REPEAT或FORWARD_AND_BACK保证时间递增14.9 小结VSG 动画 Animation含若干SamplerAnimationManagerTransformSampler用TransformKeyframes位置/旋转/缩放驱动object通常是MatrixTransform用viewer-animationManager-play(animation)注册随viewer-update()自动推进相机/骨骼/形变动画分别对应CameraSampler/JointSampler/MorphSampler。14.10 延伸阅读与下一章预告第 15 章《纹理与材质》给动画模型贴上会动的纹理第 11 章《相机与视图》用CameraSampler做相机漫游第 24 章《模型查看器》轨道相机 加载 动画整合。