ROS2与Gazebo11入门教程-制作动画模型(actor)
说明:
- 介绍如何制作动画模型(actor)
概述
本节教程将说明如何使用Gazebo的“actors”来创建脚本动画。
如果想要让实体遵循仿真中的预定义路径而不受物理引擎的影响,则动画会很有用。例如,这意味着它们不会因受重力作用而掉落下来或者与其他物体发生碰撞。但是,它们将会具有可通过RGB相机看到的3D可视化效果以及可被基于GPU的深度传感器检测到的3D网格。
本节教程将详细说明如何创建不与仿真中其余部分进行交互的开环轨迹。最后,我们会快速浏览一个示例插件,该插件会根据来自环境的反馈对动画进行控制。
Actors
在Gazebo中,动画模型被称为“actor”。Actor扩展了普通的模型,增加了动画功能。
Gazebo中有两类动画,它们可以单独使用或组合在一起使用:
骨架(Skeleton)动画,是一个模型中各链接之间的相对运动。
沿某个轨迹(trajectory)的动画,这种动画会携带着actor的所有链接作为一个组合在仿真世界中运动。
可以将这两类运动组合起来,以实现在仿真世界中移动的骨架动画。
Gazebo的actor就像模型一样,因此可以像往常一样在actor中放置链接和关节。
它与一般模型的主要区别在于:
Actor始终是静态的(即,没有任何作用力作用在actor上,不管是重力、接触力或其他作用力);
Actor支持从COLLADA和BVH文件导入的骨架动画;
Actor可以具有直接在SDF中用脚本定义的轨迹;
无法在actor内嵌套模型,因此仅限于使用动画网格、链接和关节。
可以在这里查看SDF文件中
元素的完整说明。
脚本轨迹(Scripted trajectories)
- 脚本轨迹是actor的高层次动画,指定了在特定时间要达到的一系列位姿。
- Gazebo负责在这些位姿之间插入动作,以使运动流畅。
示例仿真世界
- 下面来看一下Gazebo自带的一个简单示例仿真世界,运行命令:
gazebo worlds/animated_box.world
- 这样就会看到一个浮动盒子一次又一次地沿一个正方形轨迹移动。 该轨迹经过仿真世界中的四个点([-1,-1、1],[-1、1、1],[1、1、1]和[1,-1、1]),且两点之间的移动均耗时1秒 。
仿真世界说明
可以在这里看到整个仿真世界的描述。下面逐部分对其进行说明。
首先是定义一个具有地平面和太阳的仿真世界。
<?xml version="1.0" ?>
<sdf version="1.6">
<world name="default">
<!-- A ground plane -->
<include>
<uri>model://ground_plane</uri>
</include>
<!-- A global light source -->
<include>
<uri>model://sun</uri>
</include>
- 然后创建一个名为animated_box的actor,并赋予其一个具有盒子视觉的简单链接:
<!-- An actor -->
<actor name="animated_box">
<link name="link">
<visual name="visual">
<geometry>
<box>
<size>.2 .2 .2</size>
</box>
</geometry>
</visual>
</link>
- 现在来到了actor的特别部分——script标签。首先告知actor永远重复循环及加载仿真世界后立即开始循环。
<script>
<loop>true</loop>
<delay_start>0.000000</delay_start>
<auto_start>true</auto_start>
- 在script标签中,可以使用以下参数:
loop参数:将此参数设置为true可使脚本不停地重复循环。为获得流畅的连续运动,请确保最后一个航路点与第一个航路点重合,我们会在下面看到这一点。
delay_start参数:用于设置启动脚本之前等待的时间(以秒为单位)。如果以循环方式运行,则在开始每个循环之前等待的时间都与此参数设置的时间等长。
auto_start参数:如果需要在仿真开始后立即开始播放动画,则将此参数设置为true。例如,如果仅在由插件触发时才开始播放动画,则将其设置为false会很有用。
最后定义了具有一系列航路点的一个轨迹:
<trajectory id="0" type="square">
<waypoint>
<time>0.0</time>
<pose>-1 -1 1 0 0 0</pose>
</waypoint>
<waypoint>
<time>1.0</time>
<pose>-1 1 1 0 0 0</pose>
</waypoint>
<waypoint>
<time>2.0</time>
<pose>1 1 1 0 0 0</pose>
</waypoint>
<waypoint>
<time>3.0</time>
<pose>1 -1 1 0 0 0</pose>
</waypoint>
<waypoint>
<time>4.0</time>
<pose>-1 -1 1 0 0 0</pose>
</waypoint>
</trajectory>
</script>
</actor>
</world>
</sdf>
在trajectory标签内,可以描述要遵循的一系列关键帧。该标签有两个属性:一个唯一的id和一个type(类型)。在下一节中说明骨架动画时,属性type将会非常有用。
轨迹具有如下参数:
waypoint参数:在一个轨迹中可以有任意数量的航路点(waypoint)。每个航路点都包含一个time和一个pose参数:
time参数:从脚本开始算起到应该到达该位姿的时间(以秒为单位)。
pose参数:应到达的位姿。
定义航路点的顺序并不重要,它们会遵循给定的时间。
作为一个整体,轨迹是平滑的。这意味着可以获得流畅的运动,但是可能无法达到航路点中包含的确切位姿
非actor模型也可以遵循脚本轨迹,但这需要使用插件。请参阅这个教程以了解其具体实现方法
骨架动画
Gazebo支持两种不同的骨架动画文件格式:COLLADA(.dae)和Biovision Hierarchy(.bvh)。
下面来试下Gazebo自带的一个简单示例文件。首先,创建一个新的世界文件,命令为:
gedit walk.world
- 并将以下SDF代码粘贴到该世界文件中,该SDF具有一个太阳和一个使用walk.dae作为其皮肤的actor:
<?xml version="1.0" ?>
<sdf version="1.6">
<world name="default">
<include>
<uri>model://sun</uri>
</include>
<actor name="actor">
<skin>
<filename>walk.dae</filename>
</skin>
</actor>
</world>
</sdf>
- 在Gazebo中查看一下这个仿真世界,那样您就会看到一个人在原地行走。
gazebo walk.world
皮肤
上面示例中的actor非常简单,加载的只是
标签中描述的一个COLLADA文件。 如果您之前已经制作了自定义的Gazebo模型,则可能已经将COLLADA文件用作模型的视觉和碰撞元素。当在链接中使用时,会忽略COLLADA动画,但在skin中使用时,就会加载这些动画!
元素
中指定的文件可以是绝对路径,例如:
/home/<user>/my_gazebo_models/skeleton_model/skeleton.dae
- 也可以告知Gazebo在环境变量GAZEBO_MODEL_PATH所包含的所有目录中查找网格,如下所示:
model://skeketon_model/skeleton.dae
- 最后,可以使用Gazebo自带的一些示例网格,直接引用它们的文件名即可。
- 以下是可用的网格文件列表。试试看用其中一些网格替代上面walk.world中的网格会产生什么不同的效果!
moonwalk.dae
run.dae
sit_down.dae
sitting.dae
stand_up.dae
stand.dae
talk_a.dae
talk_b.dae
walk.dae
动画
组合不同的皮肤和动画
有时,将不同的皮肤与不同的动画进行组合会很有用。
Gazebo允许用户从一个文件中获取皮肤,而从另一个文件中获取动画,只要它们具有兼容的骨架即可。
例如,文件walk.dae和moonwalk.dae是兼容的,因此可以相互混合。行走者有一件绿衬衫,而月球漫步者有一件红衬衫。
如果想要让月球漫步者穿上绿衬衫,则可以用“ walk”作为皮肤,用“ moonwalk”作为动画。
如果想让行走者穿上红衬衫,则可以用Moonwalk作为皮肤,而用walk作为动画。
标签animation与skin标签是放在一起的,且animation标签有一个name参数。
如下所示:
<?xml version="1.0" ?>
<sdf version="1.6">
<world name="default">
<include>
<uri>model://sun</uri>
</include>
<actor name="actor">
<skin>
<filename>walk.dae</filename>
</skin>
<animation name="animation">
<filename>moonwalk.dae</filename>
</animation>
</actor>
</world>
</sdf>
- 查看这个教程以了解有关COLLADA动画的更多信息。 Gazebo环境中,用作皮肤的COLLADA文件必须有<library_effects>和<library_materials>元素,而动画文件必须有<library_animations>元素。如果要一起使用,两个文件必须具有相同的<library_geometries>,<library_controllers>和<library_visual_scenes>元素。
同步动画和轨迹
至此,您已经学习了有关创建轨迹和加载静态动画的所有知识。 现在是时候学习如何将它们组合起来了。
您可能会想:“我只要将
, 和 标签添加到我的actor中,它们就可以一起正常工作”。 我不会阻止您这样想,您可以继续这样尝试,而且我甚至还会举一个这样的例子:
<sdf version="1.6">
<world name="default">
<include>
<uri>model://sun</uri>
</include>
<actor name="actor">
<skin>
<filename>walk.dae</filename>
</skin>
<animation name="animation">
<filename>walk.dae</filename>
</animation>
<script>
<trajectory id="0" type="walking">
<waypoint>
<time>0</time>
<pose>0 2 0 0 0 -1.57</pose>
</waypoint>
<waypoint>
<time>2</time>
<pose>0 -2 0 0 0 -1.57</pose>
</waypoint>
<waypoint>
<time>2.5</time>
<pose>0 -2 0 0 0 1.57</pose>
</waypoint>
<waypoint>
<time>7</time>
<pose>0 2 0 0 0 1.57</pose>
</waypoint>
<waypoint>
<time>7.5</time>
<pose>0 2 0 0 0 -1.57</pose>
</waypoint>
</trajectory>
</script>
</actor>
</world>
</sdf>
- 继续并加载它,看看会发生什么。那不是您所期望的,对吧? Actor的腿根本没有动。那是因为Gazebo不知道哪个动画与哪个轨迹相匹配。
- 所以,下面来更改一下动画名称以匹配如下所示的轨迹类型:
<animation name="walking">
<filename>walk.dae</filename>
</animation>
好的,现在actor既会在仿真世界上来回移动,也会移动他的腿了。
但那看起来不是很自然,对吧?他的脚是在地面上滑动。
骨架动画在x轴上包含一个平移分量,正如在运行没有任何轨迹的动画中所看到的那样。
但是该动画尚未与轨迹同步。可以通过在
元素中将<interpolate_x>设置为true来启用动画与轨迹同步的功能。
<animation name="walking">
<filename>walk.dae</filename>
<interpolate_x>true</interpolate_x>
</animation>
y或z轴没有interpolate标签,因此请确保动画沿x轴移动
现在,终于可以完美同步播放这两个动画了。您应该会看到这个人从一侧走到另一侧,在一个方向上走得更快,而在另一方向上走得慢
- 尝试在航路点上设置不同的时间和距离,您会发现actor的腿会根据轨迹不同移动得更快或者更慢。
闭环轨迹
您刚刚学习了如何通过SDF创建actor和设置其轨迹。那样做的局限性在于轨迹是在开环中运行的,也就是说,不会从环境中获得任何反馈。下面我们来看一个如何使用插件动态更改轨迹的示例。
如果您不熟悉Gazebo插件,请先阅读一些插件教程。
Gazebo中有一个示例仿真世界,其中有一个actor在避开障碍物四处移动。
先来看一下它的运行情况:
gazebo worlds/cafe.world
- 运行结果看起来像下面这个视频这样:https://www.youtube.com/watch?v=nZN07_5r568
SDF中的插件
- 就像模型一样,可以为任何actor编写自定义插件,并在SDF描述中指派该插件。
- 现在来看一下cafe.world中引用了视频中一个actor的那部分代码:
<actor name="actor1">
<pose>0 1 1.25 0 0 0</pose>
<skin>
<filename>moonwalk.dae</filename>
<scale>1.0</scale>
</skin>
<animation name="walking">
<filename>walk.dae</filename>
<scale>1.000000</scale>
<interpolate_x>true</interpolate_x>
</animation>
<plugin name="actor1_plugin" filename="libActorPlugin.so">
<target>0 -5 1.2138</target>
<target_weight>1.15</target_weight>
<obstacle_weight>1.8</obstacle_weight>
<animation_factor>5.1</animation_factor>
<ignore_obstacles>
<model>cafe</model>
<model>ground_plane</model>
</ignore_obstacles>
</plugin>
</actor>
- 可以看到,不是给出要遵循的一个具体航路点列表,而是提供了一个插件。在plugin标签内,有几个参数可以专门针对该插件进行调整。这里不会详细介绍插件的工作方式,这里的目的在于表明可以公开一些参数,而且确定轨迹的逻辑将会位于插件内部。
插件C++代码
可以在这里找到ActorPlugin的源代码。而其头文件则在此处。
第一个技巧就是侦听仿真世界更新开始事件,如下所示:
this->connections.push_back(event::Events::ConnectWorldUpdateBegin(
std::bind(&ActorPlugin::OnUpdate, this, std::placeholders::_1)));
- 这样就指定了一个回调函数ActorPlugin :: OnUpdate,该回调函数将会在每次世界迭代时被调用。正是在该回调函数中对actor的轨迹进行更新的。下面来看看插件在该函数中做了什么事情:
void ActorPlugin::OnUpdate(const common::UpdateInfo &_info)
{
// Time delta
double dt = (_info.simTime - this->lastUpdate).Double();
ignition::math::Pose3d pose = this->actor->GetWorldPose().Ign();
ignition::math::Vector3d pos = this->target - pose.Pos();
ignition::math::Vector3d rpy = pose.Rot().Euler();
double distance = pos.Length();
// Choose a new target position if the actor has reached its current
// target.
if (distance < 0.3)
{
this->ChooseNewTarget();
pos = this->target - pose.Pos();
}
- 该函数首先检查当前信息,例如时间和actor位姿。如果actor已经到达目标位置,则选择一个新的目标位置。
// Normalize the direction vector, and apply the target weight
pos = pos.Normalize() * this->targetWeight;
// Adjust the direction vector by avoiding obstacles
this->HandleObstacles(pos);
// Compute the yaw orientation
ignition::math::Angle yaw = atan2(pos.Y(), pos.X()) + 1.5707 - rpy.Z();
yaw.Normalize();
// Rotate in place, instead of jumping.
if (std::abs(yaw.Radian()) > GZ_DTOR(10))
{
pose.Rot() = ignition::math::Quaterniond(1.5707, 0, rpy.Z()+
yaw.Radian()*0.001);
}
else
{
pose.Pos() += pos * this->velocity * dt;
pose.Rot() = ignition::math::Quaterniond(1.5707, 0, rpy.Z()+yaw.Radian());
}
// Make sure the actor stays within bounds
pose.Pos().X(std::max(-3.0, std::min(3.5, pose.Pos().X())));
pose.Pos().Y(std::max(-10.0, std::min(2.0, pose.Pos().Y())));
pose.Pos().Z(1.2138);
- 然后在考虑障碍物和确保运动平滑的情况下继续计算目标位姿。 以下步骤是最重要的,因为它们涉及特定于actor的API。
// Distance traveled is used to coordinate motion with the walking
// animation
double distanceTraveled = (pose.Pos() -
this->actor->GetWorldPose().Ign().Pos()).Length();
this->actor->SetWorldPose(pose, false, false);
this->actor->SetScriptTime(this->actor->ScriptTime() +
(distanceTraveled * this->animationFactor));
this->lastUpdate = _info.simTime;
}
首先用SetWorldPose函数将actor的世界位姿设置为静态模型。 但是,这样做将不会触发动画。触发动画要通过使用SetScriptTime函数来告知actor它应该在其骨架动画的哪一个点中来实现。
总之,在编写自己的插件时,可以使用您选择的逻辑定义每个时间步的期望位姿。另外也不要忘记选择适当的脚本时间来同步动画。 可以在此处查看physics :: Actor类的完整API。
参考:
获取最新文章: 扫一扫右上角的二维码加入“创客智造”公众号