WTF WPF!

BC2CC’s Animated Background Explained

If you haven’t used the Command Center yet, you can install the latest 1.0 binaries here.   When I first started working on the project I wanted to give the application a bit of a ‘WOW’ factor that every other competing tool lacked.  My team has the good fortune to have more than one developer to devote to the project so it allows us to spend some time on polishing the application.  The first thing you’ll notice when starting it is that the background is quite similar to the main menu of Bad Company 2 — certainly not the most original concept in the world, but it’s a consistent and instantly recognizable connection.

The background is actually a WPF UserControl which consists of some very simple XAML elements and a subtle, slightly out-of-sync animation of these two separate elements.  It’s a bit of a telescoping effect which you might get while zooming in or out while looking through a telephoto lens.  WPF is great at  animating large, high-quality images without any noticeable degradation in performance, with a few caveats — the application shouldn’t be moving, and a 3D application in the background will definitely eat up the resources required to keep things smooth.  In the first few releases of the new UI, there were some things I didn’t account for and it ended up running pretty poorly for a small number of users which fundamentally altered the way I chose to implement this background.

First, it had to meet several criteria before it could run:

  • RenderTier Does the computer have the proper hardware to run the animation?
    • Ideally it will be Pixel Shader 2.0 compliant
    • If they are running it in a virtual environment, it will not meet the render tier requirements
    • If they have DWM disabled, it will be rendered in software and it will not be compliant
    • If they are running the application through a remote desktop client, it will also not be compliant
  • BFBC2Game.exe The obvious question — is the game running?  We check the current processes and if it’s found, we skip to the end animation and halt.
  • User Preference Some people don’t like the animation, which is completely acceptable.  If you’ve decided you never want to see it, it remains static.

Once these criteria are established, the BackgroundViewer user control can decide what to do.

The Markup

Below is the XAML markup for the initial, or static, display.  There are animations performed on the RenderTransform so we must declare these beforehand so the animation knows where to start.

<Canvas x:Name="PictureViewer">
	<Canvas.OpacityMask>
		<LinearGradientBrush StartPoint="0.5,1.0" EndPoint="0.50,0.0">
			<GradientStop Color="#4D000000" Offset="1"/>
			<GradientStop Color="#E5000000"/>
			<GradientStop Color="#8D000000" Offset="0.914"/>
			<GradientStop Color="#E5000000" Offset="0.513"/>
		</LinearGradientBrush>
	</Canvas.OpacityMask>
	<Image x:Name="image1" Source="/Images/Generic/keyart_2.jpg" RenderTransformOrigin="0.5,0.5" >
		<Image.RenderTransform>
			<TransformGroup>
				<ScaleTransform ScaleX="1.0" ScaleY="1.0"/>
				<SkewTransform/>
				<RotateTransform/>
				<TranslateTransform/>
			</TransformGroup>
		</Image.RenderTransform>
	</Image>
	<Image x:Name="image" Source="/Images/Generic/keyart_1.png" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5" >
		<Image.RenderTransform>
			<TransformGroup>
				<ScaleTransform/>
				<SkewTransform/>
				<RotateTransform/>
				<TranslateTransform  Y="100"/>
			</TransformGroup>
		</Image.RenderTransform>
	</Image>
</Canvas>

Next we have a very simple Storyboard which employs KeySpline to give the animation a bit of smoothness.

<Storyboard x:Key="OnLoaded">
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)" Storyboard.TargetName="image" AutoReverse="False" >
		<DiscreteDoubleKeyFrame KeyTime="0" Value="0.9"/>
		<SplineDoubleKeyFrame KeyTime="0:0:25" Value="1.1" KeySpline="0.5,0,0.5,1"/>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)" Storyboard.TargetName="image" AutoReverse="False">
		<DiscreteDoubleKeyFrame KeyTime="0" Value="0.9"/>
		<SplineDoubleKeyFrame KeyTime="0:0:25" Value="1.1" KeySpline="0.5,0,0.5,1"/>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)" Storyboard.TargetName="image" AutoReverse="False">
		<DiscreteDoubleKeyFrame KeyTime="0" Value="0"/>
		<SplineDoubleKeyFrame KeyTime="0:0:25" Value="110" KeySpline="0.5,0,0.5,1"/>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)" Storyboard.TargetName="image1" AutoReverse="False">
		<SplineDoubleKeyFrame KeyTime="0" Value="0.9"/>
		<SplineDoubleKeyFrame KeyTime="0:0:25" Value="1.1" KeySpline="0.5,0,0.5,1"/>
	</DoubleAnimationUsingKeyFrames>
	<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)" Storyboard.TargetName="image1" AutoReverse="False">
		<SplineDoubleKeyFrame KeyTime="0" Value="0.9"/>
		<SplineDoubleKeyFrame KeyTime="0:0:25" Value="1.1" KeySpline="0.5,0,0.5,1"/>
	</DoubleAnimationUsingKeyFrames>
</Storyboard>

The animations performed on the 2 separate images are actually the same, with the exception of the “soldier” in front having an extra Y-axis TranslateTransform animation to give it the effect of being two independent entities.  In earlier versions, this storyboard was repeated indefinitely while the application was running.  After some test cases, it was decided that after it had completed once, people were already connected to their servers and had stopped paying attention to it so it wasn’t necessary to continue.  I will note, that CPU utilization was minimal, in the realm of 1-2% for most systems.  What was being used was GPU instructions, which may or may not have been spoken for by another running application.

Code-Behind

I chose to initiate the Storyboard from code-behind so I would have greater control over it due to the requirements of the project.  For simplicity’s sake, you could simply <BeginStoryboard /> in XAML if you wanted to test this out in Kaxaml or something similar.

public BackgroundViewer()
{
	InitializeComponent();
	Instance = this;
	_sb = this.TryFindResource("OnLoaded") as Storyboard;
	this.Loaded += new RoutedEventHandler(BackgroundViewer_Loaded);
	this.LayoutUpdated += new EventHandler(BackgroundViewer_LayoutUpdated);
}
 
public void StopAnimation()
{
	if (_sb != null)
		_sb.Stop();
}
 
public void StartAnimation()
{
	if (_sb != null)
		_sb.Begin();
}
 
void BackgroundViewer_Loaded(object sender, RoutedEventArgs e)
{
	var capable = RenderTierHelper.GetIsTier2RenderingCapable(MainWindow.Instance);
	if (capable && !DisableAnimations)
	{
		if (_sb != null)
			_sb.Begin();
	}
}

Barring the call to an outside helper method to check the RenderTier, this is very simple code to execute.  If you run this, it runs nicely until you decide to resize the window.  Things start to get FUBAR’d pretty quickly because a Canvas doesn’t manage the layout when it changes.  The Canvas.Top and Canvas.Left positions are static in XAML and we must rearrange the images by handling the LayoutUpdated event. The images will then be centered on the Canvas no matter how we change the window’s size.

void BackgroundViewer_LayoutUpdated(object sender, EventArgs e)
{
	Canvas.SetLeft(image, (PictureViewer.ActualWidth - image.ActualWidth) / 2);
	Canvas.SetLeft(image1, (PictureViewer.ActualWidth - image1.ActualWidth) / 2);
	Canvas.SetTop(image, (PictureViewer.ActualHeight - image.ActualHeight) / 2);
	Canvas.SetTop(image1, (PictureViewer.ActualHeight - image1.ActualHeight) / 2);
}

That’s all for now!

353 Comments

Leave a Reply