VTK 动画:面向对象的设计
VTK 动画:面向对象的设计
上一篇文章 《VTK 动画:框架、流程与实现》 讲到了 VTK 的动画框架、动画流程,并给出了一个简单的 VTK 动画程序。
参考视频:https://www.bilibili.com/video/BV1vh411E7Qm
问题
这样做会出现几个问题:
- 对于动画的“主角”没有问题,但是短暂出现的“配角”会让场景变得复杂。
- 添加新的“角色”需要同时修改场景和动画。
面向对象的设计
- 动画中的每个“角色”或一组“角色”自主管理。
- 场景只提供框架,不管理内容。
实例:CueAnimator 和 vtkAnimationCueObserver
修改上一篇文章中的实例程序,主要关注 ueAnimator 和 vtkAnimationCueObserver 两个类的实现。
#include <vtkSmartPointer.h>
#include <vtkRenderWindow.h>
#include <vtkRenderer.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkInteractorStyleTrackballCamera.h>
#include <vtkSphereSource.h>
#include <vtkPolyDataMapper.h>
#include <vtkActor.h>
#include <vtkProperty.h>
#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
#include <vtkAnimationScene.h>
#include <vtkAnimationCue.h>
class CueAnimator
{
public:
CueAnimator() : Sphere(nullptr), Mapper(nullptr), Actor(nullptr) {}
~CueAnimator() { this->CleanUp(); }
void StartCue(vtkAnimationCue::AnimationCueInfo* vtkNotUsed(info), vtkRenderer* renderer)
{
this->Sphere = vtkSphereSource::New();
this->Mapper = vtkPolyDataMapper::New();
this->Mapper->SetInputConnection(this->Sphere->GetOutputPort());
this->Actor = vtkActor::New();
this->Actor->SetMapper(this->Mapper);
renderer->AddActor(this->Actor);
renderer->ResetCamera();
renderer->Render();
}
void Tick(vtkAnimationCue::AnimationCueInfo* info, vtkRenderer* renderer)
{
double new_st = info->AnimationTime * 180;
this->Sphere->SetStartTheta(new_st);
renderer->Render();
}
void EndCue(vtkAnimationCue::AnimationCueInfo* vtkNotUsed(info), vtkRenderer* renderer)
{
this->CleanUp();
}
protected:
vtkSphereSource* Sphere;
vtkPolyDataMapper* Mapper;
vtkActor* Actor;
void CleanUp()
{
if (this->Sphere)
{
this->Sphere->Delete();
Sphere = nullptr;
}
if (this->Mapper)
{
this->Mapper->Delete();
Mapper = nullptr;
}
if (this->Actor)
{
this->Actor->Delete();
Actor = nullptr;
}
}
private:
};
// 观察者/命令模式(Observer/Command)
class vtkAnimationCueObserver : public vtkCommand
{
public:
// VTK 对象都是通过内部定义静态函数 New() 来生成,用 Delete() 方法删除
static vtkAnimationCueObserver* New() { return new vtkAnimationCueObserver; }
vtkRenderer* Renderer;
vtkRenderWindow* RenWin;
CueAnimator* Animator;
// Execute() 是纯虚函数,所以从 vtkCommand 派生的类都必须实现这个方法
virtual void Execute(vtkObject* vtkNotUsed(caller), unsigned long event, void* calldata)
{
if (this->Animator && this->Renderer)
{
vtkAnimationCue::AnimationCueInfo* info = static_cast<vtkAnimationCue::AnimationCueInfo*>(calldata);
switch (event)
{
case vtkCommand::StartAnimationCueEvent:
this->Animator->StartCue(info, this->Renderer);
break;
case vtkCommand::EndAnimationCueEvent:
this->Animator->EndCue(info, this->Renderer);
break;
case vtkCommand::AnimationCueTickEvent:
this->Animator->Tick(info, this->Renderer);
break;
default:
break;
}
}
// 刷新窗口
if (this->RenWin)
this->RenWin->Render();
}
protected:
vtkAnimationCueObserver() : Renderer(nullptr), RenWin(nullptr), Animator(nullptr) {}
~vtkAnimationCueObserver()
{
if (this->Renderer)
{
this->Renderer->Delete();
Renderer = nullptr;
}
if (this->RenWin)
{
this->RenWin->Delete();
RenWin = nullptr;
}
if (this->Animator)
{
Animator = nullptr;
}
}
};
// Factory 设计模式
/*
class vtkCustomAnimationCue : public vtkAnimationCue
{
public:
// VTK 对象都是通过内部定义静态函数 New() 来生成,用 Delete() 方法删除
static vtkCustomAnimationCue* New();
// 父子继承类型宏,用来追踪父子关系。借助该宏,不需要重新实现 vtkAnimationCue 的基本函数
vtkTypeMacro(vtkCustomAnimationCue, vtkAnimationCue);
vtkRenderWindow* RenWin;
vtkSphereSource* Sphere;
protected:
// 因此构造函数、析构函数都要定义为 protected 类型
vtkCustomAnimationCue() : RenWin(nullptr), Sphere(nullptr) {}
~vtkCustomAnimationCue() {}
// Overridden to adjust the sphere'sradius depending on the frame we
// are rendering. In this animation wewant to change the StartTheta
// of the sphere from 0 to 180 over thelength of the cue.
virtual void TickInternal(double currenttime, double deltatime, double clocktime)
{
double new_st = currenttime * 180;
// since the cue is in normalizedmode, the currenttime will be in the
// range[0,1], where 0 is start ofthe cue and 1 is end of the cue.
this->Sphere->SetStartTheta(new_st);
this->RenWin->Render();
}
private:
// 而对与赋值运算符和拷贝构造函数定义为 private 类型,只做声明不用实现
vtkCustomAnimationCue(const vtkCustomAnimationCue&); // Not implemented
void operator=(const vtkCustomAnimationCue&); // Not implemented
};
// vtkStandardNewMacro 是 VTK 的一个宏,作用是定义一个通用的 New() 函数,
// 使用时需要将实际的类名传递给它,确保你创建的对象是实际类的实例,而不是基类的实例
vtkStandardNewMacro(vtkCustomAnimationCue);
*/
int main()
{
/*
// 新建一个 Source 数据源对象
vtkSmartPointer<vtkSphereSource> sphere = nullptr;
sphere = vtkSmartPointer<vtkSphereSource>::New();
// 设置属性
sphere->SetPhiResolution(30); // 设置纬度方向上的点数,默认值为 8
sphere->SetThetaResolution(30); // 设置经度方向上的点数,默认值为 8
// 新建一个 Mapper 映射器对象
vtkSmartPointer<vtkPolyDataMapper> sphereMapper = nullptr;
sphereMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
// 接受 cylinder 的输出,将数据映射为几何元素
sphereMapper->SetInputConnection(sphere->GetOutputPort());
// 新建一个 Actor 演示对象
vtkSmartPointer<vtkActor> sphereActor = nullptr;
sphereActor = vtkSmartPointer<vtkActor>::New();
// vtkActor 派生自 vtkProp 类,渲染场景中数据的可视化表达是通过 vtkProp 的子类负责的
// vtkProp 子类负责确定渲染场景中对象的位置、大小和方向信息
sphereActor->SetMapper(sphereMapper);
sphereActor->GetProperty()->SetColor(0.0, 0.0, 1.0);
*/
// 创建一个 Renderer 渲染器对象,负责管理场景的渲染过程
vtkSmartPointer<vtkRenderer> renderer = nullptr;
renderer = vtkSmartPointer<vtkRenderer>::New();
// 添加 vtkProp 类型的对象到渲染场景中
// renderer->AddActor(sphereActor);
// 设置渲染场景的背景颜色
renderer->SetBackground(1.0, 1.0, 1.0); // R、G、B,全 0 为黑色,全 1 为白色
// 创建一个 Window 窗口对象,负责本地计算机系统中窗口创建和渲染过程管理
vtkSmartPointer<vtkRenderWindow> window = nullptr;
window = vtkSmartPointer<vtkRenderWindow>::New();
window->AddRenderer(renderer);
window->SetSize(640, 480); // 设置窗口大小
window->Render();
window->SetWindowName("Sphere");
// 新建一个 Interactor 交互器对象,提供平台独立的响应鼠标、键盘和时钟事件的交互机制
vtkSmartPointer<vtkRenderWindowInteractor> interactor = nullptr;
interactor = vtkSmartPointer<vtkRenderWindowInteractor>::New();
// 设置渲染窗口,消息是通过渲染窗口捕获到的,所以必须要给交互器对象设置渲染窗口
interactor->SetRenderWindow(window);
// 新建一个交互器样式对象,该样式下,用户通过控制相机对物体作旋转、放大、缩小等操作
vtkSmartPointer<vtkInteractorStyleTrackballCamera> style = nullptr;
style = vtkSmartPointer<vtkInteractorStyleTrackballCamera>::New();
// 定义交互器样式,默认的交互样式为 vtkInteractorStyleSwitch
interactor->SetInteractorStyle(style);
vtkNew<vtkAnimationScene> scene; // 创建动画场景
//scene->SetModeToRealTime(); // 设置实时播放模式
scene->SetModeToSequence(); // 设置顺序播放模式
scene->SetFrameRate(30); // 设置帧率,单位时间内渲染的帧数
scene->SetStartTime(0); // 动画开始时间
scene->SetEndTime(60); // 动画结束时间
//vtkCustomAnimationCue* cue = vtkCustomAnimationCue::New(); // 创建动画实例
vtkNew<vtkAnimationCue> cue;
// 对于类中需要操作的对象进行赋值
//cue->Sphere = sphere;
//cue->RenWin = window;
cue->SetTimeModeToNormalized(); // 按照场景时间标准化实例的动画时间,0 对应动画场景的开始,1 对应结束
cue->SetStartTime(.0);
cue->SetEndTime(1.0);
scene->AddCue(cue); // 添加动画实例到动画场景中
CueAnimator animator;
vtkNew<vtkAnimationCueObserver> observer;
observer->Renderer = renderer;
observer->RenWin = window;
observer->Animator = &animator;
// 针对某个事件添加观察者到某个 VTK 对象中,如果所监听的事件会发生,会调用 vtkCommand 子类中的 Execute() 函数
cue->AddObserver(vtkCommand::StartAnimationCueEvent, observer);
cue->AddObserver(vtkCommand::EndAnimationCueEvent, observer);
cue->AddObserver(vtkCommand::AnimationCueTickEvent, observer);
//scene->SetLoop(1); // 设置循环播放模式
scene->Play(); // 动画场景开始播放
scene->Stop(); // 结束播放
interactor->Initialize();
interactor->Start();
return EXIT_SUCCESS;
}
运行结果:动画效果与上一篇文章的效果一样,只是动画播放完后,我们启动了交互器,于是可以对一个半球壳进行交互,比如放大、缩小、旋转、拖动等操作。
进一步优化:抽象出一个 ICueAnimator 接口类
在上面的程序中,我们实现了 CueAnimator 类,里面定义了动画的内容。我们实现了 vtkAnimationCueObserver 类,它继承自 vtkCommand 类,里面有一个 CueAnimator 指针 Animator,在其 Execute() 函数中实现了自定义动画逻辑,调用 Animator 的函数完成相应的动画。
在主程序中,将动画实例 cue 添加观察者 observer,如果所监听的事件会发生,会调用 vtkAnimationCueObserver 类中的 Execute() 函数,从而实现动画。
之后想要修改动画的内容,只需要修改 CueAnimator,而不需要修改 vtkAnimationCueObserver 和场景,从而实现了动画场景和动画内容的隔离。
但是这个框架还存在问题:无法添加新的动画元素。
改进👇:
我们定义一个 ICueAnimator 接口类,里面定义了 CueAnimator 的所有函数:StartCue、Tick、EndCue,并且都是纯虚函数。
// CueAnimator 接口类,无需实现
class ICueAnimator
{
public:
virtual void StartCue(vtkAnimationCue::AnimationCueInfo* vtkNotUsed(info), vtkRenderer* renderer) = 0;
virtual void Tick(vtkAnimationCue::AnimationCueInfo* info, vtkRenderer* renderer) = 0;
virtual void EndCue(vtkAnimationCue::AnimationCueInfo* vtkNotUsed(info), vtkRenderer* renderer) = 0;
};
复制一个 CueAnimator 类,改成 CueAnimator2,作为第二个动画。修改其中的 Tick 函数,实现区别于 CueAnimator 的动画效果。
class CueAnimator2 : public ICueAnimator
{
public:
CueAnimator2() : Sphere(nullptr), Mapper(nullptr), Actor(nullptr) {}
~CueAnimator2() { this->CleanUp(); }
void StartCue(vtkAnimationCue::AnimationCueInfo* vtkNotUsed(info), vtkRenderer* renderer)
{
this->Sphere = vtkSphereSource::New();
Sphere->SetPhiResolution(30);
Sphere->SetThetaResolution(30);
this->Mapper = vtkPolyDataMapper::New();
this->Mapper->SetInputConnection(this->Sphere->GetOutputPort());
this->Actor = vtkActor::New();
this->Actor->SetMapper(this->Mapper);
renderer->AddActor(this->Actor);
renderer->ResetCamera();
renderer->Render();
}
void Tick(vtkAnimationCue::AnimationCueInfo* info, vtkRenderer* renderer)
{
double new_st = info->AnimationTime;
this->Sphere->SetCenter(1, new_st, new_st);
renderer->Render();
}
void EndCue(vtkAnimationCue::AnimationCueInfo* vtkNotUsed(info), vtkRenderer* renderer)
{
this->CleanUp();
}
protected:
vtkSphereSource* Sphere;
vtkPolyDataMapper* Mapper;
vtkActor* Actor;
void CleanUp()
{
if (this->Sphere)
{
this->Sphere->Delete();
Sphere = nullptr;
}
if (this->Mapper)
{
this->Mapper->Delete();
Mapper = nullptr;
}
if (this->Actor)
{
this->Actor->Delete();
Actor = nullptr;
}
}
};
因为 vtkAnimationCueObserver 要观察多个 ICueAnimator 子类了,所以我们使用一个 list 数据结构存储这些 ICueAnimator*,构造函数中,不再构造单个的 CueAnimator,析构函数中,将 AnimatorList 的每个遍历赋值空指针,最后清空 AnimatorList。执行函数 Execute 中,遍历 每个 animator,执行相应的函数。
// 观察者/命令模式(Observer/Command)
class vtkAnimationCueObserver : public vtkCommand
{
public:
// VTK 对象都是通过内部定义静态函数 New() 来生成,用 Delete() 方法删除
static vtkAnimationCueObserver* New() { return new vtkAnimationCueObserver; }
vtkRenderer* Renderer;
vtkRenderWindow* RenWin;
//CueAnimator* Animator;
std::list<ICueAnimator*> AnimatorList;
// Execute() 是纯虚函数,所以从 vtkCommand 派生的类都必须实现这个方法
virtual void Execute(vtkObject* vtkNotUsed(caller), unsigned long event, void* calldata)
{
if (this->AnimatorList.size() > 0 && this->Renderer)
{
vtkAnimationCue::AnimationCueInfo* info = static_cast<vtkAnimationCue::AnimationCueInfo*>(calldata);
switch (event)
{
case vtkCommand::StartAnimationCueEvent:
for (ICueAnimator* animator : AnimatorList)
animator->StartCue(info, this->Renderer);
break;
case vtkCommand::EndAnimationCueEvent:
for (ICueAnimator* animator : AnimatorList)
animator->EndCue(info, this->Renderer);
break;
case vtkCommand::AnimationCueTickEvent:
for (ICueAnimator* animator : AnimatorList)
animator->Tick(info, this->Renderer);
break;
default:
break;
}
}
// 刷新窗口
if (this->RenWin)
this->RenWin->Render();
}
protected:
vtkAnimationCueObserver() : Renderer(nullptr), RenWin(nullptr) {}
~vtkAnimationCueObserver()
{
if (this->Renderer)
{
this->Renderer->Delete();
Renderer = nullptr;
}
if (this->RenWin)
{
this->RenWin->Delete();
RenWin = nullptr;
}
if (!this->AnimatorList.empty())
{
for (ICueAnimator* animator : AnimatorList)
{
animator = nullptr;
}
AnimatorList.clear();
}
}
};
在主函数中,新创建一个 CueAnimator2 对象,将 2 个动画对象插入 observer 的 AnimatorList:
observer->AnimatorList.push_back(&animator);
observer->AnimatorList.push_back(&animator2);
运行结果:2 个动画同时播放,互不影响。
总结
- 介绍了面向对象的 VTK 动画框架设计。
- 介绍了实现一个继承自 vtkCommand 的 vtkAnimationCueObserver 类来监听 vtkAnimationCue 的动画事件。
- 介绍了利用接口类 ICueAnimator 封装动画内容,里面只包含纯虚函数。真正的动画内容 CueAnimator 类继承接口类,自定义动画内容,多个 CueAnimator 对象插入vtkAnimationCueObserver 类的 AnimatorList 实现多个监听。
最终达到了动画场景和动画内容的隔离。
参考
- https://www.bilibili.com/video/BV1vh411E7Qm
- https://blog.csdn.net/Littlehero_121/article/details/128683252
- https://blog.csdn.net/weixin_38500110/article/details/78817071