예제 #1
0
    public ManualMeshRenderSample(Microsoft.Xna.Framework.Game game)
      : base(game)
    {
      SampleFramework.IsMouseVisible = false;
      var delegateGraphicsScreen = new DelegateGraphicsScreen(GraphicsService)
      {
        RenderCallback = Render,
      };
      GraphicsService.Screens.Insert(0, delegateGraphicsScreen);

      // Add a custom game object which controls the camera.
      _cameraObject = new CameraObject(Services);
      GameObjectService.Objects.Add(_cameraObject);

      _scene = new Scene();
      SceneSample.InitializeDefaultXnaLights(_scene);

      // For advanced users: Set this flag if you want to analyze the imported opaque data of
      // effect bindings.
      EffectBinding.KeepOpaqueData = true;

      _model = ContentManager.Load<ModelNode>("Dude/Dude").Clone();
      var meshNode = _model.GetSubtree().OfType<MeshNode>().First();
      meshNode.ScaleLocal = new Vector3F(1, 2, 1);
      var mesh = meshNode.Mesh;
      var timeline = new TimelineClip(mesh.Animations.Values.First())
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Cycle,
      };
      AnimationService.StartAnimation(timeline, (IAnimatableProperty)meshNode.SkeletonPose);
    }
예제 #2
0
    public SkinnedEffectSample(Microsoft.Xna.Framework.Game game)
      : base(game)
    {
      SampleFramework.IsMouseVisible = false;
      GraphicsScreen.ClearBackground = true;
      GraphicsScreen.BackgroundColor = Color.CornflowerBlue;
      SetCamera(new Vector3F(1, 1, 3), 0.2f, 0);

      // Create a sky mesh and add an instance of this mesh to the scene.
      var skyMesh = ProceduralSkyDome.CreateMesh(GraphicsService, ContentManager.Load<Texture2D>("sky"));
      _sky = new MeshNode(skyMesh);
      _sky.Name = "Sky"; // Always set a name - very useful for debugging!
      GraphicsScreen.Scene.Children.Add(_sky);

      // Load the skinned model. This model is processed using the DigitalRune Model 
      // Processor - not the default XNA model processor!
      // In the folder that contains dude.fbx, there are several XML files (*.drmdl and *.drmat) 
      // which define the materials of the model. These material description files are 
      // automatically processed by the DigitalRune Model Processor. Please browse 
      // to the content folder and have a look at the *.drmdl and *.drmat files.
      var dudeModel = ContentManager.Load<ModelNode>("Dude/Dude");
      dudeModel = dudeModel.Clone();
      dudeModel.PoseWorld = new Pose(Matrix33F.CreateRotationY(ConstantsF.Pi));
      GraphicsScreen.Scene.Children.Add(dudeModel);

      // The dude model consists of a single mesh.
      var dudeMeshNode = dudeModel.GetSubtree().OfType<MeshNode>().First();
      var mesh = dudeMeshNode.Mesh;

      /*
        // The dude mesh consists of different materials (head, eyes, torso, ...).
        // We could change some of the material properties...
        foreach (var material in mesh.Materials)
        {
          // Get all SkinnedEffectBindings which wrap the XNA SkinnedEffect. 
          // A material can consist of several effects - one effect for each render pass.
          // (Per default there is only one render pass called "Default".)
          foreach (var effectBinding in material.EffectBindings.OfType<SkinnedEffectBinding>())
          {
            // We could change effect parameters here, for example:
            effectBinding.PreferPerPixelLighting = true;
          }
        }
      */

      // The DigitalRune Model Processor also loads animations.
      // Start the first animation of the dude and let it loop forever.
      // (We keep the animation controller to be able to stop the animation in 
      // Dispose() below.)
      var timeline = new TimelineClip(mesh.Animations.Values.First())
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Cycle,
      };
      _animationController = AnimationService.StartAnimation(timeline, (IAnimatableProperty)dudeMeshNode.SkeletonPose);
    }
예제 #3
0
    public CustomAvatarAnimationSample(Microsoft.Xna.Framework.Game game)
      : base(game)
    {
      SampleFramework.IsMouseVisible = false;

      // Add a custom game object which controls the camera.
      _cameraObject = new CameraObject(Services);
      _cameraObject.ResetPose(new Vector3F(0, 1, -3), ConstantsF.Pi, 0);
      GameObjectService.Objects.Add(_cameraObject);

      // Create a random avatar.
      _avatarDescription = AvatarDescription.CreateRandom();
      _avatarRenderer = new AvatarRenderer(_avatarDescription);

      // Wrap the Stand0 AvatarAnimationPreset (see WrappedAnimationSample) to create an
      // infinitely looping stand animation.
      AvatarAnimation standAnimationPreset = new AvatarAnimation(AvatarAnimationPreset.Stand0);
      TimelineGroup standAnimation = new TimelineGroup
      {
        new WrappedAvatarExpressionAnimation(standAnimationPreset),
        new WrappedAvatarSkeletonAnimation(standAnimationPreset),
      };
      _standAnimation = new TimelineClip(standAnimation)
      {
        LoopBehavior = LoopBehavior.Cycle,  // Cycle the Stand animation...
        Duration = TimeSpan.MaxValue,       // ...forever.
      };

      // Load animations from content pipeline.
      _faintAnimation = ContentManager.Load<TimelineGroup>("XboxAvatars/Faint");
      _jumpAnimation = ContentManager.Load<TimelineGroup>("XboxAvatars/Jump");
      _kickAnimation = ContentManager.Load<TimelineGroup>("XboxAvatars/Kick");
      _punchAnimation = ContentManager.Load<TimelineGroup>("XboxAvatars/Punch");

      // The walk cycle should loop: Put it into a timeline clip and set a
      // loop-behavior.
      TimelineGroup walkAnimation = ContentManager.Load<TimelineGroup>("XboxAvatars/Walk");
      _walkAnimation = new TimelineClip(walkAnimation)
      {
        LoopBehavior = LoopBehavior.Cycle,  // Cycle the Walk animation...
        Duration = TimeSpan.MaxValue,       // ...forever.
      };
    }
예제 #4
0
    public AvatarAttachmentSample(Microsoft.Xna.Framework.Game game)
      : base(game)
    {
      SampleFramework.IsMouseVisible = false;

      // This sample uses Scene and MeshRenderer for rendering the attached models.
      _scene = new Scene();
      _meshRenderer = new MeshRenderer();

      // Add a custom game object which controls the camera.
      _cameraObject = new CameraObject(Services);
      _cameraObject.ResetPose(new Vector3F(0, 1, -3), ConstantsF.Pi, 0);
      GameObjectService.Objects.Add(_cameraObject);

      // Create a random avatar.
      _avatarDescription = AvatarDescription.CreateRandom();
      _avatarRenderer = new AvatarRenderer(_avatarDescription);

      // Load walk animation using the content pipeline.
      TimelineGroup animation = ContentManager.Load<TimelineGroup>("XboxAvatars/Walk");

      // Create a looping walk animation.
      _walkAnimation = new TimelineClip(animation)
      {
        LoopBehavior = LoopBehavior.Cycle,  // Cycle Walk animation...
        Duration = TimeSpan.MaxValue,       // ...forever.
      };

      var baseballBatModelNode = ContentManager.Load<ModelNode>("XboxAvatars/BaseballBat").Clone();
      _baseballBatMeshNode = baseballBatModelNode.GetChildren().OfType<MeshNode>().First();

      // If we only render the baseball bat, it appears black. We need to add it to
      // a scene with some lights. (The lights do not affect the avatar.)
      SceneSample.InitializeDefaultXnaLights(_scene);

      // We must detach the mesh node from its current parent (the model node) before
      // we can add it to the scene.
      _baseballBatMeshNode.Parent.Children.Remove(_baseballBatMeshNode);
      _scene.Children.Add(_baseballBatMeshNode);
    }
예제 #5
0
    public SceneSample(Microsoft.Xna.Framework.Game game)
      : base(game)
    {
      SampleFramework.IsMouseVisible = false;
      var delegateGraphicsScreen = new DelegateGraphicsScreen(GraphicsService)
      {
        RenderCallback = Render,
      };
      GraphicsService.Screens.Insert(0, delegateGraphicsScreen);

      // Add a custom game object which controls the camera.
      _cameraObject = new CameraObject(Services);
      GameObjectService.Objects.Add(_cameraObject);

      // Create a new empty scene.
      _scene = new Scene();

      // Add the camera node to the scene.
      _scene.Children.Add(_cameraObject.CameraNode);

      // Load a model. This model uses the DigitalRune Model Processor. Several XML 
      // files (*.drmdl and *.drmat) in the folder of dude.fbx define the materials and other properties. 
      // The DigitalRune Model Processor also imports the animations of the dude model.
      var model = ContentManager.Load<ModelNode>("Dude/Dude");

      // Add two clones of the model to the scene.
      _model0 = model.Clone();
      _model1 = model.Clone();
      _scene.Children.Add(_model0);
      _scene.Children.Add(_model1);

      // The dude model contains a single mesh node.
      var meshNode0 = (MeshNode)_model0.Children[0];
      var meshNode1 = (MeshNode)_model1.Children[0];

      // The imported animation data (skeleton and animations) is stored with the mesh.
      var animations = meshNode0.Mesh.Animations;

      // The MeshNodes of skinned models has a SkeletonPose which can be animated.
      // Let's start the first animation.
      var timeline0 = new TimelineClip(animations.Values.First())
      {
        LoopBehavior = LoopBehavior.Cycle, // Loop animation...
        Duration = TimeSpan.MaxValue,      // ...forever.
      };
      _animationController0 = AnimationService.StartAnimation(timeline0, (IAnimatableProperty)meshNode0.SkeletonPose);
      _animationController0.UpdateAndApply();

      var timeline1 = new TimelineClip(animations.Values.First())
      {
        LoopBehavior = LoopBehavior.Cycle,
        Duration = TimeSpan.MaxValue,

        // Start second animation at a different animation time to add some variety.
        Delay = TimeSpan.FromSeconds(-1),
      };
      _animationController1 = AnimationService.StartAnimation(timeline1, (IAnimatableProperty)meshNode1.SkeletonPose);
      _animationController1.UpdateAndApply();

      // Add some lights to the scene which have the same properties as the lights 
      // of BasicEffect.EnableDefaultLighting().
      InitializeDefaultXnaLights(_scene);

      _meshRenderer = new MeshRenderer();

      var spriteFont = UIContentManager.Load<SpriteFont>("UI Themes/BlendBlue/Default");
      _debugRenderer = new DebugRenderer(GraphicsService, spriteFont);
    }
예제 #6
0
        public void PlayAnimation(string name)
        {
            if (CurrentAnimation == name)
                return;

            StopAnimation();

            CurrentAnimation = name;

            // Start selected animation.
            var meshNode = ModelNode.GetDescendants().OfType<MeshNode>().First();
            var mesh = meshNode.Mesh;
            var animation = mesh.Animations[name];
            var loopingAnimation = new TimelineClip(animation)
            {
                Duration = TimeSpan.MaxValue,
                LoopBehavior = LoopBehavior.Cycle,
            };
            _animationController = _animationService.StartAnimation(loopingAnimation, (IAnimatableProperty)meshNode.SkeletonPose);

            // Update view model IsPlaying flags.
            foreach (var animationPropertyViewModel in _animationPropertyViewModels)
                if (animationPropertyViewModel.Name == CurrentAnimation)
                    animationPropertyViewModel.IsPlaying = true;
        }
예제 #7
0
    private void StartDudeAnimation(ModelNode dude)
    {
      // The dude model contains a single mesh node.
      var meshNode = (MeshNode)dude.Children[0];

      // The imported animation data (skeleton and animations) is stored with the mesh.
      var animations = meshNode.Mesh.Animations;

      // The MeshNodes of skinned models has a SkeletonPose which can be animated.
      // Let's start the first animation.
      var timeline0 = new TimelineClip(animations.Values.First())
      {
        LoopBehavior = LoopBehavior.Cycle, // Loop animation...
        Duration = TimeSpan.MaxValue,      // ...forever.
      };
      _animationController = AnimationService.StartAnimation(timeline0, (IAnimatableProperty)meshNode.SkeletonPose);
      _animationController.UpdateAndApply();
    }
예제 #8
0
        protected override void LoadContent()
        {
            // Create a random avatar.
              _avatarDescription = AvatarDescription.CreateRandom();
              _avatarRenderer = new AvatarRenderer(_avatarDescription);

              // Load walk animation using the content pipeline.
              TimelineGroup animation = Game.Content.Load<TimelineGroup>("Walk");

              // Create a looping walk animation.
              _walkAnimation = new TimelineClip(animation)
              {
            LoopBehavior = LoopBehavior.Cycle,  // Cycle Walk animation...
            Duration = TimeSpan.MaxValue,       // ...forever.
              };

              _baseballBat = Game.Content.Load<Model>("BaseballBat");

              base.LoadContent();
        }
예제 #9
0
    /// <summary>
    /// Animates the opacity and offset of a group of controls from their current value to the 
    /// specified value.
    /// </summary>
    /// <param name="controls">The UI controls to be animated.</param>
    /// <param name="opacity">The opacity.</param>
    /// <param name="offset">The offset.</param>
    private AnimationController AnimateTo(IList<UIControl> controls, float opacity, Vector2F offset)
    {
      TimeSpan duration = TimeSpan.FromSeconds(0.6f);

      // First, let's define the animation that is going to be applied to a control.
      // Animate the "Opacity" from its current value to the specified value.
      var opacityAnimation = new SingleFromToByAnimation
      {
        TargetProperty = "Opacity",
        To = opacity,
        Duration = duration,
        EasingFunction = new CubicEase { Mode = EasingMode.EaseIn },
      };

      // Animate the "RenderTranslation" property from its current value, which is 
      // usually (0, 0), to the specified value.
      var offsetAnimation = new Vector2FFromToByAnimation
      {
        TargetProperty = "RenderTranslation",
        To = offset,
        Duration = duration,
        EasingFunction = new CubicEase { Mode = EasingMode.EaseIn },
      };

      // Group the opacity and offset animation together using a TimelineGroup.
      var timelineGroup = new TimelineGroup();
      timelineGroup.Add(opacityAnimation);
      timelineGroup.Add(offsetAnimation);

      // Now we duplicate this animation by creating new TimelineClips that wrap the TimelineGroup.
      // A TimelineClip is assigned to a target by setting the TargetObject property.
      var storyboard = new TimelineGroup();

      for (int i = 0; i < controls.Count; i++)
      {
        var clip = new TimelineClip(timelineGroup)
        {
          TargetObject = controls[i].Name,  // Assign the clip to the i-th control.
          Delay = TimeSpan.FromSeconds(0.04f * i),
          FillBehavior = FillBehavior.Hold, // Hold the last value of the animation when it            
        };                                  // because we don't want to opacity and offset to
        // jump back to their original value.
        storyboard.Add(clip);
      }

      // Now we apply the "storyboard" to the group of UI controls. The animation system
      // will automatically assign individual animations to the right objects and 
      // properties.
#if !XBOX && !WP7
      var animationController = AnimationService.StartAnimation(storyboard, controls);
#else
      var animationController = AnimationService.StartAnimation(storyboard, controls.Cast<IAnimatableObject>());
#endif

      animationController.UpdateAndApply();

      // The returned animation controller can be used to start, stop, pause, ... all
      // animations at once. (Note that we don't set AutoRecycle here, because we will 
      // explicitly stop and recycle the animations in the code above.)
      return animationController;
    }
예제 #10
0
    // The following code contains two helper methods to animate the opacity and offset
    // of a group of UI controls. The methods basically do the same, they animate the 
    // properties from/to a specific value. However the methods demonstrate two different 
    // approaches.
    //
    // The AnimateFrom method uses a more direct approach. It directly starts an 
    // animation for each UI control in list, thereby creating several independently 
    // running animations.
    //
    // The AnimateTo method uses a more declarative approach. All animations are 
    // defined and assigned to the target objects by setting the name of the UI control
    // in the TargetObject property. Then all animations are grouped together into
    // a single animation. When the resulting animation is started the animation system
    // creates the required animation instances and assigns the instances to the correct
    // objects and properties by matching the TargetObject and TargetProperty with the
    // name of the UI controls and their properties.
    //
    // Both methods achieve a similar result. The advantage of the first method is more
    // direct control. The advantage of the seconds method is that only a single animation
    // controller is required to control all animations at once.

    /// <summary>
    /// Animates the opacity and offset of a group of controls from the specified value to their 
    /// current value.
    /// </summary>
    /// <param name="controls">The UI controls to be animated.</param>
    /// <param name="opacity">The initial opacity.</param>
    /// <param name="offset">The initial offset.</param>
    private void AnimateFrom(IList<UIControl> controls, float opacity, Vector2F offset)
    {
      TimeSpan duration = TimeSpan.FromSeconds(0.8);

      // First, let's define the animation that is going to be applied to a control.
      // Animate the "Opacity" from the specified value to its current value.
      var opacityAnimation = new SingleFromToByAnimation
      {
        TargetProperty = "Opacity",
        From = opacity,
        Duration = duration,
        EasingFunction = new CubicEase { Mode = EasingMode.EaseOut },
      };

      // Animate the "RenderTranslation" property from the specified offset to its
      // its current value, which is usually (0, 0).
      var offsetAnimation = new Vector2FFromToByAnimation
      {
        TargetProperty = "RenderTranslation",
        From = offset,
        Duration = duration,
        EasingFunction = new CubicEase { Mode = EasingMode.EaseOut },
      };

      // Group the opacity and offset animation together using a TimelineGroup.
      var timelineGroup = new TimelineGroup();
      timelineGroup.Add(opacityAnimation);
      timelineGroup.Add(offsetAnimation);

      // Run the animation on each control using a negative delay to give the first controls
      // a slight head start.
      var numberOfControls = controls.Count;
      for (int i = 0; i < controls.Count; i++)
      {
        var clip = new TimelineClip(timelineGroup)
        {
          Delay = TimeSpan.FromSeconds(-0.04 * (numberOfControls - i)),
          FillBehavior = FillBehavior.Stop,   // Stop and remove the animation when it is done.
        };
        var animationController = AnimationService.StartAnimation(clip, controls[i]);

        animationController.UpdateAndApply();

        // Enable "auto-recycling" to ensure that the animation resources are recycled once
        // the animation stops or the target objects are garbage collected.
        animationController.AutoRecycle();
      }
    }
예제 #11
0
        protected override void LoadContent()
        {
            _avatarDescription = AvatarDescription.CreateRandom();
              _avatarRenderer = new AvatarRenderer(_avatarDescription);

              // Wrap the Stand0 AvatarAnimationPreset (see WrappedAnimationSample) to create an
              // infinitely looping stand animation.
              AvatarAnimation standAnimationPreset = new AvatarAnimation(AvatarAnimationPreset.Stand0);
              TimelineGroup standAnimation = new TimelineGroup
              {
            new WrappedExpressionAnimation(standAnimationPreset),
            new WrappedSkeletonAnimation(standAnimationPreset),
              };
              _standAnimation = new TimelineClip(standAnimation)
              {
            LoopBehavior = LoopBehavior.Cycle,  // Cycle the Stand animation...
            Duration = TimeSpan.MaxValue,       // ...forever.
              };

              // Load animations from content pipeline.
              _faintAnimation = Game.Content.Load<TimelineGroup>("Faint");
              _jumpAnimation = Game.Content.Load<TimelineGroup>("Jump");
              _kickAnimation = Game.Content.Load<TimelineGroup>("Kick");
              _punchAnimation = Game.Content.Load<TimelineGroup>("Punch");

              // The walk cycle should loop: Put it into a timeline clip and set a
              // loop-behavior.
              TimelineGroup walkAnimation = Game.Content.Load<TimelineGroup>("Walk");
              _walkAnimation = new TimelineClip(walkAnimation)
              {
            LoopBehavior = LoopBehavior.Cycle,  // Cycle the Walk animation...
            Duration = TimeSpan.MaxValue,       // ...forever.
              };

              base.LoadContent();
        }
예제 #12
0
    public ProxyNodeSample(Microsoft.Xna.Framework.Game game)
      : base(game)
    {
      SampleFramework.IsMouseVisible = false;
      var delegateGraphicsScreen = new DelegateGraphicsScreen(GraphicsService)
      {
        RenderCallback = Render,
      };
      GraphicsService.Screens.Insert(0, delegateGraphicsScreen);

      _renderer = new MeshRenderer();

      // Add a custom game object which controls the camera.
      _cameraObject = new CameraObject(Services);
      GameObjectService.Objects.Add(_cameraObject);

      _scene = new Scene();
      SceneSample.InitializeDefaultXnaLights(_scene);

      // For advanced users: Set this flag if you want to analyze the imported opaque data of
      // effect bindings.
      EffectBinding.KeepOpaqueData = true;

      // Original model in scene graph.
      var modelNode = ContentManager.Load<ModelNode>("Dude/Dude").Clone();
      modelNode.PoseLocal = new Pose(new Vector3F(-2, 0, 0));
      var meshNode = modelNode.GetSubtree().OfType<MeshNode>().First();
      _scene.Children.Add(modelNode);

      // Clone referenced by proxy node.
      var modelNode2 = modelNode.Clone();
      var meshNode2 = modelNode2.GetSubtree().OfType<MeshNode>().First();
      meshNode2.SkeletonPose = meshNode.SkeletonPose;
      _proxyNode = new ProxyNode(null)
      {
        Name = "Proxy",
        PoseLocal = new Pose(new Vector3F(2, 0, 0), Matrix33F.CreateRotationY(ConstantsF.Pi)),
        ScaleLocal = new Vector3F(0.5f),
      };
      _scene.Children.Add(_proxyNode);
      _proxyNode.Node = modelNode2;

      var spriteFont = UIContentManager.Load<SpriteFont>("UI Themes/BlendBlue/Default");
      _debugRenderer = new DebugRenderer(GraphicsService, spriteFont);

      var mesh = meshNode.Mesh;
      foreach (var m in mesh.Materials)
      {
        //((ConstParameterBinding<Vector3>)m["Default"].ParameterBindings["SpecularColor"]).Value = new Vector3();
        ((SkinnedEffectBinding)m["Default"]).PreferPerPixelLighting = true;
      }

      var timeline = new TimelineClip(mesh.Animations.Values.First())
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Cycle,
      };
      AnimationService.StartAnimation(timeline, (IAnimatableProperty)meshNode.SkeletonPose);
    }
예제 #13
0
    private void CreateAndStartAnimations()
    {
      // Get the scene nodes that we want to animate using their names (as defined 
      // in the .fbx file).
      _frontWheelLeft = _tank.GetSceneNode("l_steer_geo");
      _frontWheelLeftRestPose = _frontWheelLeft.PoseLocal;
      _frontWheelRight = _tank.GetSceneNode("r_steer_geo");
      _frontWheelRightRestPose = _frontWheelRight.PoseLocal;
      _hatch = _tank.GetSceneNode("hatch_geo");
      _hatchRestPose = _hatch.PoseLocal;
      _turret = _tank.GetSceneNode("turret_geo");
      _turretRestPose = _turret.PoseLocal;
      _cannon = _tank.GetSceneNode("canon_geo");
      _cannonRestPose = _cannon.PoseLocal;

      // Create and start some animations. For general information about the DigitalRune Animation
      // system, please check out the user documentation and the DigitalRune Animation samples.

      // The front wheel should rotate left/right; oscillating endlessly.
      var frontWheelSteeringAnimation = new AnimationClip<float>(
        new SingleFromToByAnimation
        {
          From = -0.3f,
          To = 0.3f,
          Duration = TimeSpan.FromSeconds(3),
          EasingFunction = new SineEase { Mode = EasingMode.EaseInOut }
        })
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Oscillate,
      };
      AnimationService.StartAnimation(frontWheelSteeringAnimation, _frontWheelSteeringAngle)
                      .AutoRecycle();

      // The hatch opens using a bounce ease. 
      var bounceOpenAnimation = new SingleFromToByAnimation
      {
        By = -0.8f,
        Duration = TimeSpan.FromSeconds(1),
        EasingFunction = new BounceEase { Mode = EasingMode.EaseOut }
      };
      // Then it should close again.
      var bounceCloseAnimation = new SingleFromToByAnimation
      {
        By = 0.8f,
        Duration = TimeSpan.FromSeconds(0.5f),
      };
      // We combine the open and close animation. The close animation should start 
      // 2 seconds after the open animation (Delay = 2) and it should stay some 
      // time in the final position (Duration = 2).
      var bounceOpenCloseAnimation = new TimelineGroup
      {
        bounceOpenAnimation,
        new TimelineClip(bounceCloseAnimation) { Delay = TimeSpan.FromSeconds(2), Duration = TimeSpan.FromSeconds(2)},
      };
      // The bounceOpenCloseAnimation should loop forever.
      var hatchAnimation = new TimelineClip(bounceOpenCloseAnimation)
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Cycle,
      };
      AnimationService.StartAnimation(hatchAnimation, _hatchAngle)
                      .AutoRecycle();

      // The turret rotates left/right endlessly.
      var turretAnimation = new AnimationClip<float>(
        new SingleFromToByAnimation
        {
          From = -0.5f,
          To = 0.5f,
          Duration = TimeSpan.FromSeconds(4),
          EasingFunction = new HermiteEase { Mode = EasingMode.EaseInOut }
        })
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Oscillate,
      };
      AnimationService.StartAnimation(turretAnimation, _turretAngle)
                      .AutoRecycle();

      // The cannon rotates up/down endlessly.
      var cannonAnimation = new AnimationClip<float>(
        new SingleFromToByAnimation
        {
          By = -0.7f,
          Duration = TimeSpan.FromSeconds(6),
          EasingFunction = new HermiteEase { Mode = EasingMode.EaseInOut }
        })
      {
        Duration = TimeSpan.MaxValue,
        LoopBehavior = LoopBehavior.Oscillate,
      };
      AnimationService.StartAnimation(cannonAnimation, _cannonAngle)
                      .AutoRecycle();
    }