Object model
The animation functionality in ArcScene is implemented by a set of objects listed in Figure 1. Conceptually, an animation is a description of the changes of a set of objects in time. To describe how an object changes the animation framework uses keyframes, snapshots of the object’s state at different moments. From these keyframes the state of the object at any given time can be calculated, interpolated between the keyframes.
Figure 1: Animation object model
Each kind of object has different properties that can be animated. The AnimationType classes describe these properties for each specific type. The 3D Analyst installation includes three out-of-the-box animation types: Scene, Camera and Layer. Each animation type is associated to a different kind of keyframe, so there are three predefined keyframe classes that store configurations of each kind of animated object.
All the keyframe classes (SceneKeyframe, LayerKeyframe and Bookmark3D) implement a common interface (IKeyframe) used to access and apply the stored properties. Notice that Bookmark3D class is used both as camera keyframe and as 3D bookmark. For this reason it implements an additional interface that can be used to convert bookmarks into camera keyframes.
Individual keyframes can be used to animate an object, but in most cases you will prefer to create an AnimationTrack, a group of keyframes meant to control the same object or objects. If you want to take advantage of ArcScene’s animation tools to easily work with multiple animation tracks you need to add them to the Scene (by using the IAnimationTracks interface).
Setting property values in keyframes
The IKeyframe interface contains methods to set the values of the properties stored in the keyframe and apply them directly to the object. To set a value in the keyframe we need to know the index and the type of the property. The sorted list of property names for each animation type can be seen by selecting the Keyframes or Tracks tab of the Animation Manager dialog box and clicking on the Properties button.
The names and types of each property can also be obtained programmatically by using the IAnimationType interface in the corresponding AnimationType object. For convenience, all the property names and their types are listed in the Appendix A of this document.
For example, the code sample 1 listed in Appendix B creates a single layer keyframe and sets the value of the Z rotation angle. This angle is part of the Rotation property, a Point-type property with index 4. The Point type contains three floating-point numbers, in this case corresponding to the rotation angles around the X, Y and Z axes. The following code is used to set the Z rotation value:
[VBA]
Dim pAngles As IPoint
Set pAngles = New Point
pAngles.X = 0
pAngles.Y = 0
pAngles.Z = angle
pKeyframe.PropertyValuePoint(4) = pAngles
When a keyframe is created, all its properties are active by default. If you don’t use some of them in the animation and you want to allow the user to change them interactively, we must explicitly list the active properties. In the code sample 1, only the Rotation property will be active in the keyframe.
[VBA]
Dim pProps As ILongArray
Set pProps = New LongArray
pProps.Add (4)
pKeyframe.ActiveProperties = pProps
In the Camera Keyframe (the Bookmark3D object) there is an internal limitation on the properties that can be active. When the projection mode is Ortho all the properties but the ortho extent and the projection mode itself will be inactive. In the Perspective projection mode the ortho extent property will be inactive. For this reason it is important to remember that the projection mode must be set before you assign the list of active properties.
When you want to use keyframes to interpolate the state of an object, whether the keyframes are added to an animation track or not, you must set their time stamps. In the case of unbound keyframes the time stamps define the interval in which it is correct to call the IKeyframe::Interpolate method. For instance, in the code sample 2 we use two keyframes to create an interpolated rotation. The first keyframe has time stamp 0.0 and the second has 1.0. This means we can call the Interpolate method in the first keyframe and pass the second one as argument to interpolate from time 0.0 to 1.0.
When we add the keyframes to an animation track the time stamp must be between the 0.0 and 1.0 values that represent respectively the beginning and the end of the track. Notice that the keyframe time stamps do not refer to the total span of the animation, but to the interval occupied by the track. The time stamps do not change when the Begin/End times of the track change.
An important note is that the time stamps must be assigned AFTER adding the keyframes to the track. If you set the time value before, this value will be overwritten by the automatic time stamp assignation that the track performs (see below).
Using unbound keyframes to control objects
We can use a single keyframe to set the state of an object by using the IKeyframe::Apply method. Usually this doesn’t make much sense, since you can set the state of the object directly by using its own interfaces. There is one case, however, when this technique is useful: the transformation of layers [1]. The transformation properties for layers (translation, rotation, etc.) are currently accessible only through the LayerKeyframe object, so an easy way to move a layer around is to create a LayerKeyframe, set the transformation properties and apply the keyframe to the layer.
The code sample 1 uses this technique to make a layer rotate 360 degrees. The animation is created by a simple loop that changes the value of the rotation angle, calls the apply method to transform the layer and refreshes the viewers.
[VBA]
Dim angle As Integer
For angle = 0 To 360
Dim pAngles As IPoint
Set pAngles = New Point
pAngles.X = 0
pAngles.Y = 0
pAngles.Z = angle
pKeyframe.PropertyValuePoint(4) = pAngles ' set rotation
pKeyframe.Apply pScene, pLayer ' apply it
pSG.RefreshViewers
Next angle
A more convenient way to animate an object is to create two or more keyframes that define the state of the object at different times and then interpolate the state of the object in between. In this case the values in the keyframes remain static during the animation, but the object changes because it will smoothly evolve [2] from the state specified in one keyframe to the next one.
The code sample 2 shows how to use two keyframes to create a simple rotation animation without adding them to an animation track. In this case a rotation value (0) is set to the first keyframe, and another (90) to the second keyframe. For the interpolation to be possible we need to set time stamp values (0.0 and 1.0 in this example). Then we can make a loop that smoothly changes the property 4 (the Rotation) from the first to the second keyframe, by using the IKeyframe::Interpolate method:
[VBA]
Dim pAnimType As IAnimationType
Set pAnimType = New AnimationTypeLayer
Dim Time As Double
Dim iteration As Integer
For iteration = 0 To 100
Time = iteration / 100#
' reset
pAnimType.ResetObject pScene, pLayer
' interpolate state of the object
pKeyframe1.Interpolate pScene, pLayer, 4, Time, pKeyframe2, pKeyframe1, pKeyframe2
' refresh object
If (pKeyframe1.ObjectNeedsRefresh) Then
pKeyframe1.RefreshObject pScene, pLayer
End If
pSG.RefreshViewers
Next iteration
The key of this animation loop is the Interpolate call. To this call we pass a time value that must be between the time stamps of the first and second keyframes (0.0 and 1.0). When we interpolate between two keyframes we must invoke the Interpolate method in the first keyframe and pass the second keyframe as fifth argument. Then, why do we pass again pKeyframe1 and pKeyframe2 as sixth and seventh arguments? The interpolate method can create smooth changes by using more than two keyframes [3]. In general, it accepts four (Figure 2), but if you have only two available you can reuse them in the Interpolate call as we do in the sample 2.
Layer transformations are also different from other properties because the values interpolated from the keyframes can be accumulated to create complex effects. For this reason we need to reset the transformation for each new animation iteration (hence the call to ResetObject) and explicitly refresh the layer with the resulting accumulated transformation (call to RefreshObject). Other animation types may not require these calls, but it is a good practice to use them.
Figure 2: General case of interpolation with four keyframes, and simple case with only two keyframes.
Angle interpolation in LayerKeyframe
The precise behavior of the interpolation might be different for each animation type, since it is implemented in each keyframe. In the case of layer keyframes, when moving from one rotation angle to another the interpolation will always follow the shortest way. This is very convenient to create smooth movements. Unfortunately, it means that you cannot make a full 360-turn by adding one keyframe with angle 0 and another with angle 360 because the shortest path between them is followed by staying at angle 0. In this case we would need at least four keyframes, for instance at 0, 120, 240 and 360 degrees.
A similar problem happens with 180 turns. When angles are separated 180 degrees, there are two possible ways to rotate (clockwise and counter-clockwise) and both are equal in length. If you want to make sure that the object rotates in the direction you want, you should add a third intermediate keyframe.
Using an animation track to control an object
The previous example used two keyframes that were not inserted in an animation track. However, in a usual animation you may need many keyframes and properties. The script code to create the corresponding interpolations could become very complex. It is much more convenient to add the keyframes (with their proper property values and time stamps) to an animation track and then use the IAnimationTrack interface to perform the interpolation.
The code sample 3 in the Appendix B shows a simple use of this technique to create a 360 degrees rotation. First, an animation track with the proper animation type is created, and then a number of keyframes are added with their rotation values and time stamps:
[VBA]
Dim pKeyframe As iKeyframe
Dim nKeyframes As Integer
nKeyframes = 4
Dim iKeyframe As Integer
For iKeyframe = 0 To nKeyframes - 1
Set pKeyframe = New LayerKeyframe
pAnimTrack.InsertKeyframe pKeyframe, -1 ‘ insert last
pKeyframe.TimeStamp = 1# * iKeyframe / (nKeyframes - 1)
' set rotation values
Dim pAngles As IPoint
Set pAngles = New Point
pAngles.X = 0
pAngles.Y = 0
pAngles.Z = 360# * iKeyframe / (nKeyframes - 1)
pKeyframe.PropertyValuePoint(4) = pAngles
Next iKeyframe
The active properties for the track can be set, optionally, and -most important- the track must be attached to the object or objects whose evolution will control. The animation loop now looks like this:
[VBA]
Dim Time As Double
Dim iteration As Integer
For iteration = 0 To 100
Time = iteration / 100#
' reset
pAnimType.ResetObject pScene, pLayer
' interpolate by using track
pAnimTrack.InterpolateObjectProperties pScene, Time
pSG.RefreshViewers
Next iteration
The key of this loop is the call to the InterpolateObjectProperties method of the track. This method will find the appropriate keyframes and active properties to interpolate for all the objects attached to the track. Notice that the call to ResetObject is still necessary but the call to RefreshObject is internally performed by the track.
The default time stamp assigned to a keyframe when it is inserted in an animation track depends on the value of the IAnimationTrack::EvenTimeStamps boolean property. When this flag is true the time stamps of the keyframes are automatically redistributed in equally spaced intervals between 0.0 and 1.0. When the flag is false, the default time stamp is 1.0.
You can have full control of the time stamps assigned to the keyframes by disabling the EvenTimeStamps flags and setting the time value after the keyframe is added to the track. It is your responsibility to make sure that the order of the keyframes corresponds to increasing time values. Otherwise the interpolation results are not well defined.
Using the scene as container of the animation tracks
In the previous samples we have used animation tracks that remain unbound, not connected to the scene. As a consequence, these tracks are not visible in the Animation Manager dialog box of ArcScene and cannot be played, exported to video or persisted in the document. If you want to use these facilities you need to add the animation tracks to the scene. The following code can be used for this purpose:
[VBA]
' add the track to the scene
Dim pSceneTracks As IAnimationTracks
Set pSceneTracks = pScene
pSceneTracks.AddTrack pAnimTrack
' send event to inform
Dim pActiveView As IActiveView
Set pActiveView = pScene
pActiveView.ContentsChanged
Figure 3: Keyframes view in the ArcScene Animation Manager dialog box after running code sample 3 and adding the track to the scene
Figure 3 shows how the Keyframes page of the Animation Manager dialog box will look after running sample code 3 and adding the track to the scene. Note the four keyframes with their time stamps. Scrolling to the right you would be able to see the four different Rotation Z values (0, 120, 240, 360).
In addition, when we add multiple animation tracks to the scene you can make an animation loop without calling each track to interpolate the attached objects. In this loop ISceneTracks::ApplyTracks will effectively perform all the object changes described in the tracks. This method accepts two time arguments: the total duration of the animation and the elapsed time for which we want to interpolate. It is the responsibility of the caller to calculate in some way the elapsed time. A simple option is to use a counter a counter variable that increases in the loop between the initial and ending time values.
The sample code 4 shows how to calculate the real elapsed time in Visual Basic and use it to play the animation currently defined in the scene with a desired duration.
[VBA]
Dim duration As Double
duration = 10
Dim startTime As Single
startTime = Timer
Do
Dim elapsedTime As Single
elapsedTime = Timer - startTime
If (elapsedTime > duration) Then elapsedTime = duration
pSceneTracks.ApplyTracks pSG.ActiveViewer, elapsedTime, duration
pSG.ActiveViewer.Redraw True
Loop While elapsedTime < duration
This basic example can be adapted to create custom play controls and also develop your own video export tools. Something important to notice is the role of the first argument (a reference to a scene viewer) in the call to ApplyTracks. If you pass a specific viewer to ApplyTracks and then you redraw it, like in the example, only that viewer will be animated. But if you want to animate multiple viewers simultaneously (you need first to set up the appropriate track bindings [4]) pass Nothing (a null pointer) as first argument in ApplyTracks, and refresh all the viewers involved.
Defining custom animation types
The out-of-the-box animation types use properties of different 3D Analyst objects (Scene, Layer, Camera) that can be modified efficiently. Other properties of these objects (e.g. the base height or the symbology of a layer) have not been included because it would take some time to recreate the display after each change and the animation could not be played with an acceptable frame rate. However, even expensive changes can be successfully used to generate video files, because their playing speed does not depend on how fast the video was created in the first place.
These remarks suggest that you might be interested in defining custom animation types that control properties or objects other than the ones predefined in the 3D Animation Types. In addition, it is also possible to define animation types that control objects defined externally to ArcScene, including COM objects that you have implemented.
In order to create a custom animation type you have to implement both a Keyframe and an AnimationType class, and register the AnimationType class in the 3D Animation Types component category. After doing this, the animation commands and tools in ArcScene will recognize the new type, and you will be able to create keyframes and tracks of the custom type, edit their properties in the Animation Manager, persist, play and export to video the resulting animation. Optionally, you can also develop specific tools to work with your custom animation type.
Let’s say we have developed a COM class Radar that implements the interface IRadar, and we want to define a custom animation that controls three properties of this object, the type of radar, its azimuth angle and its color. We are going to focus here on the implementation of the animation objects, included in the sample code 5 of Appendix B.
The AnimationType implementation
The mission of the AnimationType component is to offer information about the specific properties of each animation type, and allow clients to identify the objects it can animate.
Let’s start with the methods used to identify the animated objects. First we need to specify which type of objects can be animated with our custom type (the Radar object). We do that by implementing the AppliesToObject method.
[VBA]
Private Property Get IAnimationType_AppliesToObject(ByVal pObject As Variant) As Boolean
' check that pObject is of type Radar
IAnimationType_AppliesToObject = (TypeOf pObject Is IRadar)
End Property
The client may want to find all the objects of the right type that are available to be animated. In this example we assume that we have a global pRadars array (of type Array) where we store the Radar objects, so we just need to return the array.
[VBA]
Private Property Get IAnimationType_ObjectArray(ByVal pScene As IScene) As IArray
' return array with Radar objects
Set IAnimationType_ObjectArray = pRadars
End Property
Each animated object needs to have a unique ID assigned. This is required to persist the animation. Usually this ID is the index of the object is an array or list, but it can be any long integer that defines a unique object of its type. The value -1 is an invalid ID. First we need to implement a method that returns the ID for a given object (passed as a Variant with an Unknown pointer type). In our case this just means finding the index of that Radar in our global array.
[VBA]
Private Property Get IAnimationType_AnimationObjectID(ByVal pScene As IScene, ByVal pObject As Variant) As Long
' return index of Radar(index)=pObject
Dim pRadarObj As IRadar
Set pRadarObj = pObject
Dim pRadar As IRadar
Dim lIndex As Long
For lIndex = 0 To (pRadars.Count - 1)
Set pRadar = pRadars.Element(lIndex)
If pRadar Is pRadarObj Then
IAnimationType_AnimationObjectID = lIndex
Exit Property
End If
Next lIndex
IAnimationType_AnimationObjectID = -1
End Property
Then we must also implement a method that returns an object given its ID. In our case we use the ID as index in the global Radar array.
[VBA]
Private Property Get IAnimationType_AnimationObjectByID(ByVal pScene As IScene, ByVal objectID As Long) As Variant
' return Radar(objectID)
Set IAnimationType_AnimationObjectByID = pRadars.Element(objectID)
End Property
The client (as the ArcScene application) may want to display a name for each available object (so, for instance, the user can chose one in a dropdown list). In our case we assume that each Radar has a name assigned, but the name could also be generated from the ID (e.g. "radar 0", "radar 1"...).
[VBA]
Private Property Get IAnimationType_AnimationObjectName(ByVal pScene As IScene, ByVal pObject As Variant) As String
' return string for Radar equal to pObject
Dim pRadar As IRadar
Set pRadar = pObject
IAnimationType_AnimationObjectName = pRadar.Name
Exit Property
End Property
Other important information the client may request is the one referred to the object properties that the custom animation type controls. In our example we assume three properties, the type, the azimuth angle and the color.
[VBA]
Private Property Get IAnimationType_PropertyCount() As Long
IAnimationType_PropertyCount = 3
End Property
Private Property Get IAnimationType_PropertyName(ByVal Index As Long) As String
Select Case Index
Case 0
IAnimationType_PropertyName = "Azimuth"
Case 1
IAnimationType_PropertyName = "Color"
Case 2
IAnimationType_PropertyName = "Type"
Case Else
IAnimationType_PropertyName = "unknown"
End Select
End Property
In order to use the methods of the Keyframe object to read and write property values, the client must know what is the type of each property. In our example, the azimuth is a double and the color is an RGB color.
[VBA]
Private Property Get IAnimationType_PropertyType(ByVal Index As Long) As esriAnimationPropertyType
Select Case Index
Case 0
IAnimationType_PropertyType = esriAnimationPropertyDouble
Case 1
IAnimationType_PropertyType = esriAnimationPropertyRGBColor
Case 2
IAnimationType_PropertyType = esriAnimationPropertyInt
End Select
End Property
Long and integer properties can be used to represent enumerations. For instance, in the Camera animation type the Projection Mode property is an enumeration with two values (see Appendix A). In our example the Type property is also an enumeration with two values: "Type A" and "Type B". We need to codify each of those values as an integer, for example 1 = type A, 2 = type B.
[VBA]
Private Property Get IAnimationType_IsEnumProperty(ByVal Index As Long) As Boolean
' only the "Type" property is enumerated
If (Index = 2) Then IAnimationType_IsEnumProperty = True
Else IAnimationType_IsEnumProperty = False
End If
End Property
We have to specify the maximum and minimum values allowed for our enumerated properties, and give a symbolic name to each of those valid values.
[VBA]
Private Property Get IAnimationType_EnumPropertyMaxValue(ByVal Index As Long) As Long
If (Index = 2) Then IAnimationType_EnumPropertyMaxValue = 2
Else IAnimationType_EnumPropertyMaxValue = -1
End If
End Property
Private Property Get IAnimationType_EnumPropertyMinValue(ByVal Index As Long) As Long
If (Index = 2) Then IAnimationType_EnumPropertyMinValue = 1
Else IAnimationType_EnumPropertyMinValue = -1
End If
End Property
Private Property Get IAnimationType_EnumPropertyValueName(ByVal Index As Long, ByVal Value As Long) As String
If (Index = 2) Then
Select Case Value
Case 1
IAnimationType_EnumPropertyValueName = "Type A"
Case 2
IAnimationType_EnumPropertyValueName = "Type B"
Case Else
IAnimationType_EnumPropertyValueName = "Type unknown"
End Select
End If
End Property
A last role of the animation type class is the identification of the COM components. First, we supply a name that will identify the animation type to the user.
[VBA]
Private Property Get IAnimationType_Name() As String
IAnimationType_Name = "Radar"
End Property
We also need to inform clients of the class ID of the animation type and its associated keyframe object, so they can instantiate and compare classes. In VisualBasic the class IDs are assigned when the DLL file is built and they can be inspected by using the OLE View tool. The UIDs must be then defined as constants in the code and returned in the proper methods.
[VBA]
Const clsidRadarAnimType = "{63BD4248-E644-4B4D-B44D-D1D69BB45243}"
Const clsidRadarKeyframe = "{A21A11E7-0C32-47FA-B960-41238EF3CC6F}"
Private Property Get IAnimationType_CLSID() As IUID
Dim objid As New UID
objid = clsidRadarAnimType
Set IAnimationType_CLSID = objid
End Property
Private Property Get IAnimationType_KeyframeCLSID() As IUID
Dim objid As New UID
objid = clsidRadarKeyframe
Set IAnimationType_KeyframeCLSID = objid
End Property
The Keyframe implementation
The other key piece to develop a custom animation type is the implementation of a Keyframe class that connects to the object to be animated (the Radar in our example). In this class we need to declare a number or private members that store information about the object properties.
[VBA]
Private sName As String ' name of keyframe
Private Const lPropertyCount As Long = 3
Private bIsActive(lPropertyCount) As Boolean
Private dTimeStamp As Double
Private bObjectNeedsRefresh As Boolean
' property values
Private dAzimuth As Double
Private pColor As IRgbColor
Private iType As Integer
In the class initialization we assign a predefined keyframe name and initialize all the properties as active. We could initialize the property values as well.
[VBA]
Private Sub Class_Initialize()
sName = "Radar Keyframe"
bObjectNeedsRefresh = False
Dim lProp As Long
For lProp = 0 To (lPropertyCount - 1)
bIsActive(lProp) = True
Next lProp
End Sub
The methods for reading and writing the name and time stamp are trivial, so we don’t reproduce them here. More interesting are the methods that read and write the active properties.
[VBA]
Private Property Let IKeyframe_ActiveProperties(ByVal arrayIndices As ILongArray)
If (Not arrayIndices Is Nothing) Then
Dim lProp As Long
For lProp = 0 To (lPropertyCount - 1)
bIsActive(lProp) = False
Next lProp
Dim lElements As Long
lElements = arrayIndices.Count
For lProp = 0 To (lElements - 1)
Dim lProperty As Long
lProperty = arrayIndices.Element(lProp)
If (lProperty >= 0 And lProperty < lPropertyCount) Then
bIsActive(lProperty) = True
End If
Next lProp
End If
End Property
Private Property Get IKeyframe_ActiveProperties() As ILongArray
Dim pArray As ILongArray
Set pArray = New LongArray
Dim lProp As Long
For lProp = 0 To (lPropertyCount - 1)
If (bIsActive(lProp)) Then pArray.Add lProp
Next lProp
Set IKeyframe_ActiveProperties = pArray
End Property
The methods for the "IsActiveProperty" property just read or write the boolean value in the bIsActive vector of the class. A read-only property of the keyframe is its animation type. In this method we must return a reference to our custom AnimationType object. In the code below we are returning a new instance in each call, but in fact we could return always the same instance if it is defined as a global variable.
[VBA]
Private Property Get IKeyframe_AnimationType() As IAnimationType
Set IKeyframe_AnimationType = New RadarAnimationType
End Property
Then we need to implement the read and write methods for the types that correspond to our properties (double for "azimuth", RGB color for "color" and enumerated integer for "Type").
[VBA]
Private Property Let IKeyframe_PropertyValueDouble(ByVal propIndex As Long, ByVal num As Double)
If (propIndex = 0) Then
dAzimuth = num
End If
End Property
Private Property Get IKeyframe_PropertyValueDouble(ByVal propIndex As Long) As Double
If (propIndex = 0) Then
IKeyframe_PropertyValueDouble = dAzimuth
End If
End Property
Private Property Let IKeyframe_PropertyValueRGBColor(ByVal propIndex As Long, ByVal color As IColor)
If (propIndex = 1) Then
Set pColor = color
End If
End Property
Private Property Get IKeyframe_PropertyValueRGBColor(ByVal propIndex As Long) As IColor
If (propIndex = 1) Then
Set IKeyframe_PropertyValueRGBColor = pColor
End If
End Property
Private Property Let IKeyframe_PropertyValueInt(ByVal propIndex As Long, ByVal num As Long)
If (propIndex = 2) Then
Set iType = num
End If
End Property
Private Property Get IKeyframe_PropertyValueInt(ByVal propIndex As Long) As Long
If (propIndex = 2) Then
Set IKeyframe_PropertyValueInt = iType
End If
End Property
The keyframe properties can also be assinged to the keyframe from an object of the right type (this happens when we use the CreateKeyframe command in ArcScene). For this to work we have to implement the following method (we assume that the Radar object has properties that match the ones in the keyframe).
[VBA]
Private Sub IKeyframe_CaptureProperties(ByVal pScene As IScene, ByVal pObject As Variant)
' set keyframe props from pObject (a Radar object) props
Dim pRadar As IRadar
Set pRadar = pObject
If (Not pRadar Is Nothing) Then
dAzimuth = pRadar.Azimuth
Set pColor = pRadar.Color
iType = pRadar.Type
End If
End Sub
The opposite operation is to set the state of the object from the property values stored in the keyframe. The object may need an additional call to be refreshed after some of its properties have changed. In our example we assume the IRadar::Update method must be called to refresh the Radar object.
[VBA]
Private Sub IKeyframe_Apply(ByVal pScene As IScene, ByVal pObject As Variant)
' set properties in pObject (a Radar) from the props of this keyframe
Dim pRadar As IRadar
Set pRadar = pObject
If (Not pRadar Is Nothing) Then
pRadar.Azimuth = dAzimuth
Set pRadar.Color = pColor
pRadar.Type = iType
End If
pRadar.Update ‘ refresh Radar
End Sub
And now we arrive at the core of the implementation of your custom animation type, the Interpolate method. Here is where you need to do some math and calculate what would be the value of a property between two keyframes (or four, see Figure 2). In our example we will perform a simple linear interpolation of the continuous properties (the azimuth and the color) between the current keyframe and the next one, ignoring pPrevKeyframe and pAfterNextKeyframe. For the discrete property (the type) we will always use the value in the current keyframe.
[VBA]
Private Sub IKeyframe_Interpolate(ByVal pScene As IScene, ByVal pObject As Variant, ByVal propertyIndex As Long, ByVal Time As Double, ByVal pNextKeyframe As IKeyframe, ByVal pPrevKeyframe As IKeyframe, ByVal pAfterNextKeyframe As IKeyframe)
Dim pRadar As IRadar
Set pRadar = pObject
If (pRadar Is Nothing) Then Exit Sub
' discrete property (type): use value in this keyframe
If (propertyIndex = 2) Then
pRadarType = iType
bObjectNeedsRefresh = True
Exit Sub
End If
' continuous interpolation
If (pNextKeyframe Is Nothing) Then Exit Sub
Dim dNextTime As Double
dNextTime = pNextKeyframe.TimeStamp
If (dNextTime <= dTimeStamp Or Time < dTimeStamp Or Time > dNextTime) Then Exit Sub ' invalid times
Dim dTimeFactor As Double
dTimeFactor = (Time - dTimeStamp) / (dNextTime - dTimeStamp)
Select Case propertyIndex
Case 0 ' dAzimuth
Dim dNextAzimuth As Double
dNextAzimuth = pNextKeyframe.PropertyValueDouble(propertyIndex)
Dim dAzimuth_Interp As Double
If (dNextAzimuth < dAzimuth) Then
dAzimuth_Interp = dAzimuth - (dTimeFactor * (dAzimuth - dNextAzimuth))
Else
dAzimuth_Interp = dAzimuth + (dTimeFactor * (dNextAzimuth - dAzimuth))
End If
pRadar.Azimuth = dAzimuth_Interp
bObjectNeedsRefresh = True
Case 1 ' pColor
Dim pNextColor As IRgbColor
Set pNextColor = pNextKeyframe.PropertyValueRGBColor(propertyIndex)
Dim pColor_Interp As IRgbColor
Set pColor_Interp = New RgbColor
pColor_Interp.Red = pColor.Red + (dTimeFactor * (pNextColor.Red - pColor.Red))
pColor_Interp.Green = pColor.Green + (dTimeFactor * (pNextColor.Green - pColor.Green))
pColor_Interp.Blue = pColor.Blue + (dTimeFactor * (pNextColor.Blue - pColor.Blue))
Set pRadar.Color = pColor_Interp
bObjectNeedsRefresh = True
End Select
End Sub
Notice that the Interpolate method only calculates the value of one property in each call. This is why we don’t refresh automatically the object inside Interpolate: an object with many properties would be refreshed each time one of them is updated, and that would be very inefficient. Instead whenever one of the properties has changed we raise the bObjectNeedsRefresh flag. The client readd this flag with the ObjectNeedsRefresh method to find out whether it needs to refresh the object or not. This is what ArcScene does when the animation is played (see also the sample code 2).
[VBA]
Private Property Get IKeyframe_ObjectNeedsRefresh() As Boolean
IKeyframe_ObjectNeedsRefresh = bObjectNeedsRefresh
End Property
If the client finds that the object has been changed by the Interpolate method, it will call RefreshObject. In our example we call the Update method in the Radar to refresh it.
[VBA]
Private Sub IKeyframe_RefreshObject(ByVal pScene As IScene, ByVal pObject As Variant)
Dim pRadar As IRadar
Set pRadar = pObject
If (Not pRadar Is Nothing) Then pRadar.Update
bObjectNeedsRefresh = False
End Sub
Registration
In order to make ArcScene recognize the custom animation type we need to register it in the category of animation types. A simple way to perform the registration is use the Categories.exe utility in the ArcGIS binary folder. In the program, search for the "ESRI 3D Animation Types" category and add the DLL file of your custom animation type.
Another option is to add directly the appropriate registry entry. You can do that by creating a text file with the .reg extension and then double-clicking on it. The file should look like this:
REGEDIT4
; This Registry Script enters a CoClass Into a Component Category
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{custom type ID}\Implemented Categories\{5980E69C-A95E-11d5-B2A0-00508BCDDE28}]
; This Registry Script enters a CoClass Into a Component Category
[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{custom type ID}\Implemented Categories\{5980E69C-A95E-11d5-B2A0-00508BCDDE28}]
where the contents from [HKEY_LOCAL through the end of the file are all on a single line.
The registration can also be done programmatically by using the category manager. Below is a simple routine that uses this mechanism to register a test animation type.
[VBA]
Sub Register()
Const cCATID_DDDAnimationTypes = _
"{5980E69C-A95E-11d5-B2A0-00508BCDDE28}"
Const cCLSID_TestAnimType = _
"{custom type ID here}"
Dim pCCM As IComponentCategoryManager
Set pCCM = New ComponentCategoryManager
Dim catid As New UID
Dim objid As New UID
objid = cCLSID_TestAnimType
catid = cCATID_DDDAnimationTypes
pCCM.SetupObject "my_path\TestAnimationType.dll", objid, catid, True
End Sub
Appendix A: properties of out-of-the-box animation types
Camera Properties
|
Name
|
Type
|
0
|
Projection Type
|
Long*
|
1
|
Target
|
Point
|
2
|
Azimuth
|
Double
|
3
|
Inclination
|
Double
|
4
|
Roll
|
Double
|
5
|
Distance
|
Double
|
6
|
View Angle
|
Double
|
7
|
Ortho Extent
|
Extent
|
(*) This is an enumerated type. The possible values are 0 = Perspective, 1 = Ortho.
Layer Properties
|
Name
|
Type
|
0
|
Visibility
|
Boolean
|
1
|
Transparency
|
Integer
|
2
|
Translation
|
Point
|
3
|
Scale
|
Point
|
4
|
Rotation
|
Point
|
5
|
Center offset
|
Point
|
Scene Properties
|
Name
|
Type
|
0
|
Vertical Exaggeration
|
Double
|
1
|
Sun Azimuth
|
Integer
|
2
|
Sun Inclination
|
Integer
|
3
|
Sun Contrast
|
Integer
|
4
|
Background Color
|
RGB Color
|
Appendix B: Code samples
Sample 1
[VBA]
' VBA macro to create an animated rotation of the first layer
' by using the Apply method in a single Layer Keyframe
Public Sub RotateLayer1( )
' get first layer in the scene
Dim pDoc As ISxDocument
Set pDoc = ThisDocument
Dim pScene As IScene
Set pScene = pDoc.Scene
Dim pLayer As ILayer
Set pLayer = pScene.Layer(0)
If (pLayer Is Nothing) Then Exit Sub
' create keyframe
Dim pKeyframe As IKeyframe
Set pKeyframe = New LayerKeyframe
' set active properties (only rotation)
Dim pProps As ILongArray
Set pProps = New LongArray
pProps.Add (4)
pKeyframe.ActiveProperties = pProps
' animation loop
Dim pSG As ISceneGraph
Set pSG = pScene.SceneGraph
Dim angle As Integer
For angle = 0 To 360
Dim pAngles As IPoint
Set pAngles = New Point
pAngles.X = 0
pAngles.Y = 0
pAngles.Z = angle
pKeyframe.PropertyValuePoint(4) = pAngles ' set rotation
pKeyframe.Apply pScene, pLayer ' apply it
pSG.RefreshViewers
Next angle
End Sub
Sample 2
' VBA macro to create an animated rotation of the first layer
' by interpolating between two layer keyframes
Public Sub RotateLayer2( )
' get first layer in the scene
Dim pDoc As ISxDocument
Set pDoc = ThisDocument
Dim pScene As IScene
Set pScene = pDoc.Scene
Dim pLayer As ILayer
Set pLayer = pScene.Layer(0)
If (pLayer Is Nothing) Then Exit Sub
' create initial and final keyframe
Dim pKeyframe1 As IKeyframe
Set pKeyframe1 = New LayerKeyframe
Dim pKeyframe2 As IKeyframe
Set pKeyframe2 = New LayerKeyframe
pKeyframe1.TimeStamp = 0#
pKeyframe2.TimeStamp = 1#
' set active properties (only rotation)
Dim pProps As ILongArray
Set pProps = New LongArray
pProps.Add (4)
pKeyframe1.ActiveProperties = pProps
pKeyframe2.ActiveProperties = pProps
' set initial and final rotation values
Dim pAngles As IPoint
Set pAngles = New Point
pAngles.X = 0
pAngles.Y = 0
pAngles.Z = 0 ' initial azimuth rotation = 0
pKeyframe1.PropertyValuePoint(4) = pAngles
pAngles.Z = 90 ' final azimuth rotation = 90
pKeyframe2.PropertyValuePoint(4) = pAngles
' animation loop
Dim pSG As ISceneGraph
Set pSG = pScene.SceneGraph
Dim pAnimType As IAnimationType
Set pAnimType = New AnimationTypeLayer
Dim Time As Double
Dim iteration As Integer
For iteration = 0 To 100
Time = iteration / 100#
' reset object (only some animation types -layer- need this)
pAnimType.ResetObject pScene, pLayer
' interpolate state of the object
pKeyframe1.Interpolate pScene, pLayer, 4, Time, pKeyframe2, pKeyframe1, pKeyframe2
' refresh object (only some animation types -layer- need this)
If (pKeyframe1.ObjectNeedsRefresh) Then
pKeyframe1.RefreshObject pScene, pLayer
End If
pSG.RefreshViewers
Next iteration
End Sub
Sample 3
' VBA macro to create an animated rotation of the first layer
' by adding keyframes to an animation track
Public Sub RotateLayer3()
' get first layer in the scene
Dim pDoc As ISxDocument
Set pDoc = ThisDocument
Dim pScene As IScene
Set pScene = pDoc.Scene
Dim pLayer As ILayer
Set pLayer = pScene.Layer(0)
If (pLayer Is Nothing) Then Exit Sub
' create an animation track
Dim pAnimTrack As IAnimationTrack
Set pAnimTrack = New AnimationTrack
' set the type before adding keyframes
Dim pAnimType As IAnimationType
Set pAnimType = New AnimationTypeLayer
Set pAnimTrack.AnimationType = pAnimType
' create four keyframes and add them to the track
Dim pKeyframe As iKeyframe
Dim nKeyframes As Integer
nKeyframes = 4
Dim iKeyframe As Integer
For iKeyframe = 0 To nKeyframes - 1
Set pKeyframe = New LayerKeyframe
pAnimTrack.InsertKeyframe pKeyframe, -1
pKeyframe.TimeStamp = 1# * iKeyframe / (nKeyframes - 1)
' set rotation values
Dim pAngles As IPoint
Set pAngles = New Point
pAngles.X = 0
pAngles.Y = 0
pAngles.Z = 360# * iKeyframe / (nKeyframes - 1)
pKeyframe.PropertyValuePoint(4) = pAngles
Next iKeyframe
' set active properties in the track (only rotation)
Dim pProps As ILongArray
Set pProps = New LongArray
pProps.Add (4)
pAnimTrack.ActiveProperties = pProps
' attach the track to the layer
pAnimTrack.AttachObject pLayer
' animation loop
Dim pSG As ISceneGraph
Set pSG = pScene.SceneGraph
Dim Time As Double
Dim iteration As Integer
For iteration = 0 To 100
Time = iteration / 100#
' reset
pAnimType.ResetObject pScene, pLayer
' interpolate by using track
pAnimTrack.InterpolateObjectProperties pScene, Time
pSG.RefreshViewers
Next iteration
End Sub
Sample 4
' VBA macro to play the current animation in the scene
' using the system clock to time the duration
Public Sub PlayAnimation()
Dim pDoc As ISxDocument
Set pDoc = ThisDocument
Dim pScene As IScene
Set pScene = pDoc.Scene
Dim pSceneTracks As IAnimationTracks
Set pSceneTracks = pScene
' animation loop
Dim pSG As ISceneGraph
Set pSG = pScene.SceneGraph
Dim duration As Double
duration = 10
Dim startTime As Single
startTime = Timer
Do
Dim elapsedTime As Single
elapsedTime = Timer - startTime
If (elapsedTime > duration) Then elapsedTime = duration
pSceneTracks.ApplyTracks pSG.ActiveViewer, elapsedTime, duration
pSG.RefreshViewers
Loop While elapsedTime < duration
End Sub
[1] - For details about how layer transformation works, see the Advanced Animation in ArcScene technical paper available on the ArcGIS Desktop Technical Papers page on the ESRI Online Support Center site.
[2] - In this discussion we assume a continuous property. Discrete properties (like the Projection Mode of a camera) are not interpolated, they remain as specified in the previous keyframe.
[3] - Exactly how smooth depends on the implementation of the interpolation of each property. Most properties whose change must be really smooth are implemented with a cubic Bezier interpolation that operates between four keyframes.
[4] - For a discussion about binding tracks to multiple objects, see the Advanced Animation in ArcScene technical paper available on the ArcGIS Desktop Technical Papers page on the ESRI Online Support Center site.