Open3D C++系列教程(四)动画 Tick事件
前置:
Open3D C++系列教程 (一)环境搭建
Open3D C++系列教程 (二)第一个GUI窗口
Open3D C++系列教程 (三)关于程序异常退出的探讨
该部分介绍如何在让场景中的物体动起来。
文章目录
1. Tick()
事件
要让场景动起来,我们就需要不断地更新场景里物体的状态。
我们可以通过为窗口设置一个TIck事件来设置物体的新状态,这个事件在每个UI Tick(约10毫秒)的时候都会被调用,从而实现动画的效果。
对于gui::Window
的实例,可以使用:
SetOnTickEvent(std::function<bool()> callback);
设置一个std::function<bool()>
类型的callback函数,并在这个函数里执行需要的动画。
这个callback函数不带任何参数,需要返回一个bool
,如果场景和ui发生了变化,则返回true
,从而进行重新绘制。
通过下面这样的形式就可以完成一个动画的设置。
auto animation = [&](){
// do something
// ...
if ( // ui or scene changed)
return true;
else
return false;
}
window->SetOnTickEvent(animation);
2. 设置一个旋转的动画
在之前的内容中,我们创建了10个几何体并直接添加到窗口中,并没有做任何的保存,为了方便进行动画,我们在创建几何体时将其数据和对应的名称保存起来。
std::vector<std::string> names;
std::vector<std::shared_ptr<open3d::geometry::Geometry3D>> geometries;
此外,我们为每个网格提供一个随机的角速度:
std::vector<double> speeds;
// 生成 (-pi/2, pi/2)的10个随机值,表示每秒旋转的度数。
std::generate_n(std::back_inserter(speeds), 10, [](){ return (rand() % 180 - 90) / 360.0 * 2 * 3.1415926; });
2.1 绕世界坐标旋转(公转)
定义一个lambda表达式表示一次旋转。
auto revolution = [&]()
{
static auto t0 = instance.Now();
auto t1 = instance.Now();
auto dt = t1 - t0;
t0 = t1;
for (int i = 0; i < 10;++i)
{
Eigen::Transform<double, 3, Eigen::Affine> transform{weak_main_scene.lock()->GetScene()->GetGeometryTransform(names[i])};
transform.prerotate(Eigen::AngleAxisd(speeds[i] * dt, Eigen::Vector3d::UnitY()));
weak_main_scene.lock()->GetScene()->SetGeometryTransform(names[i], transform.matrix());
}
return true; };
由于这是一个Tick
事件,而两次Tick
的时间几乎不可能是1s,这一般和渲染速度有关。为了使动画在1s的事件内旋转一个角度
θ
\theta
θ,我们需要计算两个Tick
之间的时间差dt
,并让物体在这次Tick
中旋转dt*\theta
度,这正是代码3-6行所做的工作。
有了时间dt
,接下来对每一个网格进行旋转。
由于每一次的旋转都是在之前旋转的基础上进行的,所以首先从场景中获取当前的几何变换(通过网格的名字来索取),并使用该变换初始化一个Eigen的仿射变换:
Eigen::Transform<double, 3, Eigen::Affine> transform{weak_main_scene.lock()->GetScene()->GetGeometryTransform(names[i])};
然后在这个变换中左乘一个新的旋转矩阵:
// 绕Y轴旋转 speeds[i] *dt 角度
transform.prerotate(Eigen::AngleAxisd(speeds[i] * dt, Eigen::Vector3d::UnitY()));
左乘和右乘
设初始化后的transform
具有一个绕 Y Y Y轴旋转 θ \theta θ的旋转 A \mathbf{A} A,此外还有一个绕 Y Y Y轴旋转 b e t a beta beta的旋转矩阵 B \mathbf{B} B,那么:
transform.prerotate(B)
等价于 B A \mathbf{BA} BA,transform.rotate(B)
等价于 A B \mathbf{AB} AB。由于在这个动画中都是围绕同一个轴进行旋转,旋转的顺序并不重要,因此在代码中使用
rotate()
和prerotate()
是等价的。
但在大部分的变换中,一般期望的都是左乘变换矩阵,所以使prerotate()
更加合适。
平移和缩放
平移和缩放同理,有pretranslate()
和translate()
,prescale()
和scale()
,一定要注意变换的顺序。
获得最终变换后就可以通过网格的名字来为网格设置新的变换:
weak_main_scene.lock()->GetScene()->SetGeometryTransform(names[i], transform.matrix());
这就完成了一次旋转变换。
网格的几何数据
这里所设置的变换不会对原始的几何数据产生任何影响,只是在渲染中起作用。
最后让该函数返回true
并将该函数设置为Tick
事件:
weak_win.lock()->SetOnTickEvent(revolution);
2.2 绕局部坐标旋转(自转)
自转与公转的代码类似,只不过在进行旋转的时候需要先将网格移动到世界原点,然后进行旋转,最后在平移回最初的位置,就可以完成绕自身旋转。
auto autorotation = [&]()
{
static auto t0 = instance.Now();
auto t1 = instance.Now();
auto dt = t1 - t0;
t0 = t1;
for (int i = 0; i < 10;++i)
{
auto transform = weak_main_scene.lock()->GetScene()->GetGeometryTransform(names[i]);
Eigen::Transform<double, 3, Eigen::Affine> composed = Eigen::Transform<double, 3, Eigen::Affine>::Identity();
composed = transform;
composed.pretranslate(-geometries[i]->GetCenter());
composed.prerotate(Eigen::AngleAxisd(2*speeds[i] * dt, Eigen::Vector3d::UnitY()));
composed.pretranslate(geometries[i]->GetCenter());
weak_main_scene.lock()->GetScene()->SetGeometryTransform(names[i], composed.matrix());
}
return true; };
在上述代码中:
- 13行:compose表示当前变换;
- 14行:将物体移动到原点,此时可以看到将几何体保存下来的作用;
- 15行:绕Y轴旋转一个角度
2*sppeds[i]*dt
; - 16行:将物体平移回之前的位置。
注意这里使用的是表示左乘的
prerotate/pretranslate
,如果直接使用rotate/translate
将会不会得到预期的结果。
3. 运行结果
3.1 公转效果
3.2 自转效果
4. 完整代码
如果不想改代码,或者想要获取直接获取源代码文件,可以通过下载链接进行下载。那么代价是什么呢?