Customizing animations for ArcScene


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}]
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]
' 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]
' 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]
' 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.