Jump to content

  • Log In with Google      Sign In   
  • Create Account






Direct2D Glass Buttons and reinventing the wheel

Posted by AvengerDr, in Odyssey UI 11 August 2014 · 370 views

Direct2D GUI library SharpDX
I am not aware of any GUI library that specifically targets C#, SharpDX and Direct2D so I had to roll my own and well this gave me the opportunity to learn how Direct2D is used. Direct2D is a wonderful API, although its usage is sometimes... hectic to say the least. Although I can understand some of the reasons behind some design choices, you cannot use it as if you were building a WPF XAML based application.

The game I am building is heavily UI-based as any self-respecting strategic game should be. I am an HCI researcher "by trade" so this was the opportunity of a lifetime to put my PhD to good use! I believe that a well-polished game needs an equally polished UI. I needed it to be able to render high-quality controls and also allow me to animate transition between states and so on. I found that Direct2D would not provide me with that kind of features, unless I implemented them. The library, as I am designing it, isn't intended to be a replacement for XAML or as a tool for data-entry applications built with DirectX (why would anyone think of such a thing? Posted Image). Therefore it is being geared towards situation that commonly occur in a 3D game or application. I'm pleased to present you the ubiquitous Glass Button made with Direct2d, SharpDX and the UI library of the Odyssey engine!

Posted Image


The cool thing about this is that I was able to get this working by following a XAML tutorial. How? Enter the... Odyssey eXstensible Interaction Language (I may need a better name..) I designed this language around XAML as it was the one I am most familiar with. The structure of a OXIL theme (!) consists of two parts. At the start of the theme file, Resource elements are declared. Resources are colors and gradients. For example:
<Resources>
    <SolidColor Name="Transparent" Color="#00000000"/>
    <SolidColor Name="Red" Color="#FFFF0000"/>
    <RadialGradient Name="ButtonFillGlow" RadiusX="1" RadiusY="1" Center="0.5, 0.8">
      <GradientStop Color="{Red}" Offset="0"/>
      <GradientStop Color="{Transparent}" Offset="1.0"/>
    </RadialGradient>
</Resources>
Gradients and colors are declared almost identically as the equivalent XAML syntax. The individual gradient stops are written in curly brackets as they reference color resources previously defined. The second part of the document is the actual theme file. Each control defines how it wants to be rendered. Let's take the above glass button as an example:
<ControlStyle Name="Button" Width="200" Height="50" TextDescriptionClass="Button">
    <VisualState>
      <Shapes>
        <Rectangle Name="Background" Position="0,0" StrokeThickness="1" Width="1" Height="1" Fill="{DimGray}" />
        <Rectangle Name="GlowEffect" Position="0,0" Width="1" Height="1" Fill="{ButtonFillGlow}"/>
        <Rectangle Name="Foreground" Position="0,0" Width="1" Height="1" Fill="{ButtonFillForeground}" Stroke="{DarkGray}"/>
        <Rectangle Name="GlassEffect" Position="0,0" Width="1" Height="0.5" Fill="{ButtonFillShine}"/>
      </Shapes>
      <Animations>
        <Animation Name="Highlighted">
          <Color4Curve TargetName="{Foreground}" TargetProperty="Fill.GradientStops[0].Color">
            <Color4KeyFrame Time="0" Value="{DarkBlue}"/>
            <Color4KeyFrame Time="1" Value="{Red}"/>
          </Color4Curve>
        </Animation>
      </Animations>
    </VisualState>
 </ControlStyle>
The ControlStyle element defines the appearance of the control and any animations attached to it, through the VisualState sub-element. In a distant future it might be possible to also specify DataTemplate elements for databinding purposes (another experimental feature supported by the library). The first part of the VisualState element lists the actual drawing instructions to the 2D renderer. Those shapes are drawn top to bottom with the last one being the foremost shape. Differently from XAML, the Position, Width and Height attributes express values relative to the control whose style this will be applied to. The width and height attributes specified at the beginning of the ControlStyle element represent the preferred values to use in absence of changes. In particular, the position attribute specifies to the renderer any offset to use while rendering the shape. A value of (0, 0) means that the shape will begin in the exact location of the host control; a value of (0, 0.5) would mean that the shape would begin in the vertical center of the control and so on. The values associated to the width and height have a similar purpose. As it stands now, it is possible to render shapes that go outside the logical boundary of the control, but they won't be part of hit-testing. The Fill and Stroke attribute reference gradient resources that were previously defined.

The animation elements can be used to define state transition. TargetName defines the Shape whose TargetProperty will be affected. The two keyframes defined successively define the animation to use. In this particular case, the change will be instantaneous. For example:

protected override void OnPointerEnter(PointerEventArgs e)
{
    base.OnPointerEnter(e);
    string animationName = ControlStatus.Highlighted.ToString();
    var animation = AnimationController[animationName];
    animation.Speed = 1.0f;
    AnimationController.Play(animationName);
}

protected override void OnPointerExited(PointerEventArgs e)
{
    base.OnPointerExited(e);
    string animationName = ControlStatus.Highlighted.ToString();
    var animation = AnimationController[animationName];
    animation.Speed = -1.0f;
    AnimationController.Play(animationName);
}
The negative speed value is used to play the animation backward. Though it would be of course possible to specify a completely different one. Under the hood, there is an object walker class that, through reflection, changes the object properties at run time. Right now, only these discrete instantaneous animations are supported But I'm working towards implementing continuous animations. This is not so easy because of how Direct2D handles resources. In Direct2D gradients are really nothing more than textures. When you specify a Direct2D gradient, it creates a texture. Therefore if you wanted to animate the "glow" color of the radial gradient in the picture, you'd have to create as many intermediate gradients as required by the length of your animation.

This is what it does right now for those two discrete keyframes. It creates a different brush as required, if they don't exist already. For two discrete keyframes it wouldn't constitute a big performance hit. However if you wanted to animate it over the course of one second at 60 fps, you'd probably have to create 60 different brushes, which might or might not be a problem. There might be alternate possibilities, such as animating the opacity of different gradients so that it would give the illusion of colours changing. Another possibility might be animating other gradient properties. For example, it should be possible to animate a RadialGradient's RadiusX and RadiusY properties. These could be used to animate a glow effect in a button that grows or pulsates, without recreating the actual resource. I will experiment on these possibilities and report back later this week. Then again, one of the objectives of this library is to give developers the means, then it will be depend on how good your UI design skills are. Which brings us to another topic....

Cool story bro, but how do I use it in my own projects?

You can check out the experimental branch of the Odyssey Engine. That's where I am currently adding these new features. The UI part of the engine can be used without most of the other libraries (which I am using for my own game). You can check out some samples in the MiniUI project. The engine itself is distributed under the terms of the GPLv3 license. Meaning that it can be freely used in academic and other open source projects. It can be used commercially (although it is still far from being commercially viable) as long as the resulting program is open-source as well (I think). These terms are not set in stone and I'm open to discussion. So if you need a UI library, don't reinvent the wheel but help me provide other SharpDX developers a good and comprehensive Gui library. Most of the groundwork is there, it needs more controls and testing. Drop me a line if you'd like to help.




Wow, looks very cool! What a pity I don't do C#, otherwise I'd give it a try, but great work!

Thanks! These are those things one usually takes for granted, then you find out that you have to do them all from scratch.. Well maybe someone will be able to back-port it to C++ someday :D

 

Anyway I did some more experiments, and the RadialGradient's radiusX/Y properties can be animated just fine:

QLYcU1y.gif

 

And that was simply achieved by typing the following lines in the XML theme file. The object walker does a good job it seems.

<Animations>
	<Animation Name="Highlighted">
	  <FloatCurve TargetName="{GlowEffect}" TargetProperty="Fill.RadiusX">
		<FloatKeyFrame Time="0" Value="0"/>
		<FloatKeyFrame Time="1.0" Value="1.5"/>
	  </FloatCurve>
	  <FloatCurve TargetName="{GlowEffect}" TargetProperty="Fill.RadiusY">
		<FloatKeyFrame Time="0" Value="0"/>
		<FloatKeyFrame Time="0.250" Value="1.5"/>
	  </FloatCurve>
	</Animation>
</Animations>

The problem I'm facing now is animating individual gradient stops. It seems that once an ID2D1Linear/RadialGradientBrush is created you can no longer modify the individual colors. Because the GetGradientStops method returns a copy. However, it seems that SolidColorBrush allows you to modify the color. I'll have to try that.

I like the delayed light-up effect!

Thanks! And for an even cooler effect, the gradient origin could be animated in the Button's mouse-over event to make it follow the pointer!

Well, let's get on with the rest of the game!  :)

Twitter

September 2014 »

S M T W T F S
 123456
78910111213
1415 16 17181920
21222324252627
282930    
PARTNERS