Visualize SDK:使用动画

Visualize SDK 允许您为实体、子实体和相机实现动画。动画 API 具有三种可能的变换:平移、旋转和缩放。

创建动画

动画容器

动画创建的第一步是创建动画容器。动画容器包含 <Actor, Action> 对,并将其绑定到时间轴,其中 Actor 定义要动画的对象,Action 定义修改规则。Visualize SDK 中的动画容器由 OdTvAnimationContainer 对象表示。可以使用相应的 OdTvDatabase 方法创建它:

OdTvAnimationContainerId contId = pDb->createAnimationContainer( OD_T( "LineAnimation" ) );
OdTvAnimationContainerPtr pCont = contId.openObject( OdTv::kForWrite );
Actor

Actor 定义在动画过程中被修改的对象。在 Visualize SDK 中,actor 由 OdTvSubItemPath 类表示,该类可以引用实体、子实体或相机对象。

Action

Action 定义修改规则,是动画开发中最复杂的阶段。由于动画容器将动作绑定到时间轴,因此动作使用帧表示来定义修改规则。一个动作包含帧。其中一些帧是您需要指定的关键帧。其他帧则使用关键帧和指定的插值方法进行插值。Visualize SDK 中的动作由 OdTvAnimationAction 对象表示。可以使用相应的 OdTvDatabase 方法创建它:

OdTvAnimationActionId actionId = pDb->createAnimationAction( OD_T( "LineRotation" ) );
OdTvAnimationActionPtr pAction = actionId.openObject( OdTv::kForWrite );

创建动作后,应对其进行配置。例如,可以指定帧数:

pAction->setNumFrames( 361 );

此外,还可以指定每秒帧数来定义动作速度。配置完成后,您需要使用以下 OdTvAnimationAction 方法指定动作关键帧:

virtual OdTvResult setKeypoint( OdUInt32 nFrame, Keydata kd, double keyval, Interpolator ipl = kCubic ) = 0;
  • nFrame — 帧号
  • kd — 要更改的对象数据
  • keyval — 此帧的实际对象数据值
  • ipl — 关键帧之间使用的插值机制

例如,以下代码创建一个包含 361 帧的动作,并使对象绕 z 轴旋转 360 度(每第 10 帧是一个关键帧):

OdTvAnimationActionId actionId = pDb->createAnimationAction( OD_T( "LineRotation" ) );
OdTvAnimationActionPtr pAction = actionId.openObject( OdTv::kForWrite );
pAction->setNumFrames( 361 );
for( OdUInt32 i = 0; i <= 360; i+=10 )
{
  double angle = (double)(i)*OdaPI / 180.0;
  pAction->setKeypoint( i, OdTvAnimationAction::kRotationZ, angle, OdTvAnimationAction::kLinear );
}

一个关键帧可以修改多个 Keydata 通道,例如:

pAction->setKeypoint( 25, OdTvAnimationAction::kTranslationX, 10, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 25, OdTvAnimationAction::kTranslationY, -10, OdTvAnimationAction::kLinear );

在此示例中,关键帧使对象沿 x 轴和 y 轴移动。

Keydata

动画动作支持三种可能的变换:平移、旋转和缩放。它们中的每一个都以 OdTvAnimationAction::Keydata 枚举中的三个通道表示:

enum Keydata
{
  kTranslationX,  //Translation by the x-axis.
  kTranslationY,  //Translation by the y-axis.
  kTranslationZ,  //Translation by the z-axis.
  kRotationX,     //Rotation by the x-axis.
  kRotationY,     //Rotation by the y-axis.
  kRotationZ,     //Rotation by the z-axis.
  kScaleX,        //Scaling by the x-axis.
  kScaleY,        //Scaling by the y-axis.
  kScaleZ,        //Scaling by the z-axis.

  kNumKeydata     //Service type: number of actual data types.
};
插值

区间 [Keyframe1;Keyframe2] 中的帧可以通过三种方式进行插值:

  • 阈值插值:在区间 [Keyframe1; Keyframe2) 上,值为 Keyframe1 的值;在 Keyframe2 帧处,值为 Keyframe2 的值
  • 线性插值
  • 三次样条插值

插值类型由 OdTvAnimationAction::Interpolator 枚举描述:

enum Interpolator
{
  kThreshold,     //No interpolation: value is equal to the previous keypoint value.
  kLinear,        //Linear interpolation.
  kCubic          //Cubic spline interpolation.
};
组合动作和执行者

指定动作和执行者后,可以使用以下 OdTvAnimationContainer 方法将其添加到动画容器中:

virtual OdTvResult addAnimation( const OdTvSubItemPath& actor, const OdTvAnimationActionId& actionId, OdInt32 nRepeats = 0, OdUInt32 timeStart = 0, OdTvActorBasis* pCustomBasis = NULL ) = 0;
};
  • nRepeats — 动画重复次数(0 – 不重复,只播放一次动画;1 – 播放并重复一次;-1 – 无限重复)
  • timeStart — 动画开始时间(毫秒)
  • pCustomBasis — 指向自定义执行者基准的指针

同一个动画动作可以与同一个或不同的动画容器中的不同执行者一起使用(您无需为每个执行者创建“360 度旋转”动作)。同样,同一个执行者可以与同一个或不同的动画容器中的不同动作一起使用。

自定义执行者基准

假设有一个执行者和一个动画动作,该动作使执行者绕 x 轴旋转 360 度:

OdTvAnimationActionPtr pAction = actionId.openObject( OdTv::kForWrite );
pAction->setNumFrames( 360 );
pAction->setKeypoint( 0, OdTvAnimationAction::kRotationX, 0.0, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 359, OdTvAnimationAction::kRotationX, 359.0 * OdaPI / 180.0, OdTvAnimationAction::kLinear );

例如,您想让执行者绕着不同的向量(例如 OdTvVector( 1.0, 1.0, 1.0 ))旋转。您可以使用上面代码中的动作并指定自定义执行者基准:

OdTvAnimationContainer::OdTvActorBasis* pBasis = new OdTvAnimationContainer::OdTvActorBasis();
pBasis->origin() = actorExtents.center();
pBasis->xAxis() = OdTvVector( 1.0, 1.0, 1.0 );
pAction->addAnimation( actor, actionId, -1, 0, pBasis );

在此示例中,执行者范围中心充当原点——这是实体动画的默认原点。OdTvActorBasis 没有限制,因此不同的轴可以是共线的且非标准化的。

播放动画

动画容器也应该用于使用以下方法播放动画:

virtual OdTvResult setCurrentTime( OdUInt32 msec )

此方法指定容器的当前时间,因此它将时间转换为每个包含并执行动作的 <执行者, 动作> 对的相应帧号。例如,以下适用于 Microsoft® Windows® 操作系统的代码将动画容器的当前时间设置为 0;接下来,它使用保存的当前时间与 clock() 方法结果之间的差值来动画化对象:

clock_t start = clock();
clock_t curTime = start;
OdUInt32 nTotalTime = 0;
Bool bInfinite = !pCont->totalTime( nTotalTime );
while( curTime - start < nTotalTime || bInfinite )
{
  pCont->setCurrentTime( curTime – start );
  pDevice->update();
  curTime = clock();
}

验证动画

当 <执行者, 动作> 对添加到动画容器时,会计算一些缓存。但是,在此执行者被修改(例如,移动)后,计算出的缓存将变为无效。请注意,动画容器不跟踪执行者或动作的更改,因此必须通过调用以下方法手动验证缓存:

virtual OdTvResult validateAnimations() = 0;

动画示例

以下示例代码生成一个 VSF 文件,其中包含一个包含三条线的模型。此文件还包含这些线的无限动画:其中两条线绕其中心点沿 z 轴旋转,一条线以重复的“8 字形”模式移动。此示例可以在 OdVisualizeViewer 应用程序中从 Examples > Animation > EntityAnimation 启动。

//1. Initial step
//1.1 Create database and model
OdTvDatabaseId dbId;
OdTvFactoryId factId = odTvGetFactory();
dbId = factId.createDatabase();
OdTvDatabasePtr pDb = dbId.openObject( OdTv::kForWrite );
OdTvModelId modelId = pDb->createModel( OD_T( "SimpleModel" ) );
OdTvModelPtr pModel = modelId.openObject( OdTv::kForWrite );
//1.2 Create entity with single line
OdTvEntityId entId = pModel->appendEntity();
OdTvEntityPtr pEnt = entId.openObject( OdTv::kForWrite );
pEnt->appendPolyline( OdTvPoint( -1.0, -1.0, 0.0 ), OdTvPoint( 1.0, 1.0, 0.0 ) );
//2. Create animation
//2.1 Create animation container
OdTvAnimationContainerId contId = pDb->createAnimationContainer( OD_T( "LineAnimation" ) );
OdTvAnimationContainerPtr pCont = contId.openObject( OdTv::kForWrite );
//2.2 Create animation action: rotation on 360 degrees around Z axis
//2.2.1 Create action
OdTvAnimationActionId actionId = pDb->createAnimationAction( OD_T( "LineRotation" ) );
OdTvAnimationActionPtr pAction = actionId.openObject( OdTv::kForWrite );
//2.2.2 Specify number of frames
pAction->setNumFrames( 361 );
//2.2.3 Each 10 frame is a key frame
for( OdUInt32 i = 0; i <= 360; i+=10 )
{
  double angle = (double)(i)*OdaPI / 180.0;
  pAction->setKeypoint( i, OdTvAnimationAction::kRotationZ, angle, OdTvAnimationAction::kLinear );
}
//2.3 Specify actor
OdTvSubItemPath actor;
actor.entitiesIds().push_back( entId );
//2.4 Add pair <Actor, Action> to the container with infinite repeats number
pCont->addAnimation( actor, actionId, -1 );

//Another animation with the same action
entId = pModel->appendEntity( OD_T( "RotatingLine2" ) );
pEnt = entId.openObject( OdTv::kForWrite );
pEnt->appendPolyline( OdTvPoint( -1.0, -1.0, 0.0 ), OdTvPoint( 1.0, 1.0, 0.0 ) );
pEnt->setModelingMatrix( OdTvMatrix::translation( OdTvVector( 3.0, 0.0, 0.0 ) ) );
contId = pDb->createAnimationContainer( OD_T( "SecondLineAnimation" ) );
pCont = contId.openObject( OdTv::kForWrite );
OdTvSubItemPath actor1;
actor1.entitiesIds().push_back( entId );
pCont->addAnimation( actor1, actionId, -1 );

//Third animation - translation
entId = pModel->appendEntity( OD_T( "MovingLine" ) );
pEnt = entId.openObject( OdTv::kForWrite );
pEnt->appendPolyline( OdTvPoint( -3.0, 1.0, 0.0 ), OdTvPoint( -3.0, -1.0, 0.0 ) );
contId = pDb->createAnimationContainer( OD_T( "ThirdLineAnimation" ) );
pCont = contId.openObject( OdTv::kForWrite );
OdTvSubItemPath actor2;
actor2.entitiesIds().push_back( entId );
actionId = pDb->createAnimationAction( OD_T( "LineTranslation" ) );
pAction = actionId.openObject( OdTv::kForWrite );
pAction->setNumFrames( 41 );
pAction->setKeypoint( 0, OdTvAnimationAction::kTranslationX, 0.0, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 0, OdTvAnimationAction::kTranslationY, 0.0, OdTvAnimationAction::kLinear );

pAction->setKeypoint( 10, OdTvAnimationAction::kTranslationX, -1.0, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 10, OdTvAnimationAction::kTranslationY, -1.0, OdTvAnimationAction::kLinear );

pAction->setKeypoint( 20, OdTvAnimationAction::kTranslationX, -1.0, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 20, OdTvAnimationAction::kTranslationY, 0.0, OdTvAnimationAction::kLinear );

pAction->setKeypoint( 30, OdTvAnimationAction::kTranslationX, 0.0, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 30, OdTvAnimationAction::kTranslationY, -1.0, OdTvAnimationAction::kLinear );

pAction->setKeypoint( 40, OdTvAnimationAction::kTranslationX, 0.0, OdTvAnimationAction::kLinear );
pAction->setKeypoint( 40, OdTvAnimationAction::kTranslationY, 0.0, OdTvAnimationAction::kLinear );

pCont->addAnimation( actor2, actionId, -1 );

//3 Create device and view, save file
{
  OdTvGsDeviceId devId = pDb->createDevice( OD_T( "SimpleDevice)" ) );
  OdTvGsDevicePtr pDev = devId.openObject( OdTv::kForWrite );
  OdTvGsViewId viewId = pDev->createView( OD_T( "SimpleView)" ) );
  OdTvGsViewPtr pView = viewId.openObject( OdTv::kForWrite );
  pView->addModel( modelId );
  pDev->addView( viewId );
  pDb->writeFile( OD_T( "D:\\LineAnimation.vsf" ) );
}

 

animation

 

另一个动画示例可以在 OdVisualizeViewer 应用程序中从“示例”>“动画”>“CameraAnimation”启动。相机动画示例:

 

animation example

 

这两个示例的源代码位置:Visualize/Examples/VisualizeModelsGenerator/VisualizeModelsGeneratorImpl.cpp。

相机动画注意事项

相机可以表示为五个不同的参与者:

1. 相机可以作为一个实体进行动画。在这种情况下,参与者应该是整个相机:

OdTvSubItemPath cameraActor;
cameraActor.entitiesIds().push_back( cameraId );

2. 只能对相机位置进行动画。在这种情况下,应使用 OdTvCamera::positionId() 来指定正确的参与者:

OdTvSubItemPath cameraPositionActor;
cameraPositionActor.entitiesIds().push_back( cameraId );
cameraPositionActor.geometryDatasIds().push_back( pCamera->positionId() );

3. 只能对相机目标进行动画。在这种情况下,应使用 OdTvCamera::targetId() 来指定正确的参与者:

OdTvSubItemPath cameraTargetActor;
cameraTargetActor.entitiesIds().push_back( cameraId );
cameraTargetActor.geometryDatasIds().push_back( pCamera->targetId() );

4. 只能对相机向上向量进行动画。在这种情况下,应使用 OdTvCamera::upId() 来指定正确的参与者:

OdTvSubItemPath cameraUpActor;
cameraUpActor.entitiesIds().push_back( cameraId );
cameraUpActor.geometryDatasIds().push_back( pCamera->upId() );

5. 只能对相机视场宽度和高度进行动画。在这种情况下,应使用 OdTvCamera::fieldsId() 来指定正确的参与者:

OdTvSubItemPath cameraFieldsActor;
cameraFieldsActor.entitiesIds().push_back( cameraId );
cameraFieldsActor.geometryDatasIds().push_back( pCamera->fieldsId() );

在这种情况下,仅使用 kScaleX 和 kScaleY 动作帧通道来缩放相机的视场宽度和视场高度,这对于动画缩放操作非常有用。

所有五个参与者的组合允许您指定全新的相机参数,而无需为所有参与者设置通用的变换规则。在这种情况下,禁用相机参数的自动调整可能很有用。例如,当启用自动调整时,指定新目标可能会改变相机向上向量,因为观察方向和向上向量必须垂直。但是向上向量动画应用于向上向量的缓存值。因此,禁用自动调整有助于避免歧义。

今天就开始行动

免费试用 ODA 软件 60 天。
无风险,无需信用卡。

免费试用