VTK 动画:面向对象的设计

VTK 动画:面向对象的设计

上一篇文章 《VTK 动画:框架、流程与实现》 讲到了 VTK 的动画框架、动画流程,并给出了一个简单的 VTK 动画程序。

参考视频:https://www.bilibili.com/video/BV1vh411E7Qm

问题

这样做会出现几个问题:

  1. 对于动画的“主角”没有问题,但是短暂出现的“配角”会让场景变得复杂。
  2. 添加新的“角色”需要同时修改场景和动画。

面向对象的设计

  1. 动画中的每个“角色”或一组“角色”自主管理。
  2. 场景只提供框架,不管理内容。

实例: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 个动画同时播放,互不影响。

在这里插入图片描述

总结

  1. 介绍了面向对象的 VTK 动画框架设计。
  2. 介绍了实现一个继承自 vtkCommand 的 vtkAnimationCueObserver 类来监听 vtkAnimationCue 的动画事件。
  3. 介绍了利用接口类 ICueAnimator 封装动画内容,里面只包含纯虚函数。真正的动画内容 CueAnimator 类继承接口类,自定义动画内容,多个 CueAnimator 对象插入vtkAnimationCueObserver 类的 AnimatorList 实现多个监听。

最终达到了动画场景和动画内容的隔离。

参考

  1. https://www.bilibili.com/video/BV1vh411E7Qm
  2. https://blog.csdn.net/Littlehero_121/article/details/128683252
  3. https://blog.csdn.net/weixin_38500110/article/details/78817071
  • 28
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值