前置:
Open3D C++系列教程 (一)环境搭建
Open3D C++系列教程 (二)第一个GUI窗口
Open3D C++系列教程 (三)关于程序异常退出的探讨
该部分介绍如何在让场景中的物体动起来。
文章目录
1. Tick()
事件
要让场景动起来,我们就需要不断地更新场景里物体的状态。
我们可以通过为窗口设置一个TIck事件来设置物体的新状态,这个事件在每个UI Tick(约10毫秒)的时候都会被调用,从而实现动画的效果。
对于gui::Window
的实例,可以使用:
SetOnTickEvent(std::function<bool()> callback);
- 1
设置一个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
</span><span class="katex-html"><span class="base"><span class="strut" style="height: 0.6944em;"></span><span class="mord mathnormal" style="margin-right: 0.0278em;">θ</span></span></span></span></span>,我们需要计算两个<code>Tick</code>之间的时间差<code>dt</code>,并让物体在这次<code>Tick</code>中旋转<code>dt*\theta</code>度,这正是代码3-6行所做的工作。</p>
有了时间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轴旋转
beta 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. 完整代码
如果不想改代码,或者想要获取直接获取源代码文件,可以通过下载链接进行下载。那么代价是什么呢?
后续