Perspective : 3D skinnable controls

Perspective : 3D skinnable controls

Reading this article requires prior knowledge of 3D development with Perspective .

Waiting for FrameworkElement3D and Control3D...

UIElement3D, published in .NET 3.5, is the first step of an architecture of 3D classes close to that of WPF 2D. Logically, the future versions of .NET should contain the FrameworkElement3D class, and then the Control3D one... Teams of Redmond will do it one day, I am sure ;-) At least I hope so. But waiting is too long, and here is one attempt…

The 3D objects of the Perspective library, in its version 0.3, were refactored to inherit from UIElement3D. Transform them into interactive controls should be logically the next step.

But the philosophy of 2D WPF controls goes further: their function is separated from their visual representation, by means of techniques such as styles and templates (implemented in the FrameworkElement and Control classes). Implementing this philosophy in 3D could appear first too ambitious for an isolated author.

And yet, without claiming to lead to a as rich and sophisticated system as WPF 2D, I realized in the version 0.4 of the Perspective library a 3D control system which respects this philosophy, by exploiting simple existing WPF techniques (resources and UIElement3D) associated with the basic 3D objects of Perspective (Box3D, Bar3D...).

This library is open-source, then inspire yourself by it, and send me your suggestions.

3D controls and skins

In the current version of Perspective, 4 interactive 3D controls classes are provided :

  • Button3D is a traditional push-button. It has a Click event, emitted when it is pressed and released.
  • Fader3D is a sliding button (see WPF 2D Slider). Its Value property depends on the position of the button, handled while dragging the mouse vertically.
  • Knob3D is a rotary button, like the volume button of an amplifier. It works like Fader3D.
  • Gyroscope3D is a gyroscope model, animated when you click on it.

The 3 first types of controls above make it possible to produce i.e. visual models of electronic devices (virtual synthetizers, etc).

Button3D, Fader3D and Knob3D

This example is available in the ButtonFaderKnob3D.xaml page of the PerspectiveDemo.UI project. Online demo

Button3D, Fader3D and Knob3D have a common ancestor: ContentControl3D.

The ContentControl3D class (namespace Perspective.Wpf3D.Primitives) inherits from UIElement3D. It has a Content property, of UIElement3D type, which is used for the visual representation of control. By assigning to it a ContainerUIElement3D Object, it is possible to build a composite visual appearence.

By default, this property is automatically loaded from the current skin. ContentControl3D uses for that the SkinManager singleton (namespace Perspective.Wpf3D).

3 skins are currently provided in the form of resources integrated into the Perspective.Wpf3D.dll assembly :

  • PsycheRock, the default skin (in reference to Pierre Henry).
  • LaMoisson (in reference to a famous painting, with which - do I have to specify ? - the contents of the skin do not have anything to see: -).
  • BlackAndWhite.

It is possible to change the current skin using the SkinManager singleton of each visual Perspective assembly (included the demo application).

private void LoadSkin(string skin)
{
    SkinManager.Current.LoadSkin(skin);
    Perspective.Wpf.SkinManager.Current.LoadSkin(skin);
    Perspective.Wpf3D.SkinManager.Current.LoadSkin(skin);
}

Skin change

In this skin, the materials are different, and the Fader3D button is spherical.

As the 3D Perspective objects all inherit from UIElement3D, it is possible to use them to customize the visual appearence of a control using its Content property. First, the OverridesDefaultSkin property must be set to true.

Example in XAML of a tap button :

<p:Knob3D 
    x:Name="myKnob" 
    Minimum="0.0" Maximum="360.0"
    OverridesDefaultSkin="True"
    MouseRightButtonDown="myKnob_MouseRightButtonDown">
    <p:Knob3D.Transform >
        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D 
                Angle="{Binding RelativeSource={RelativeSource AncestorType={x:Type p:Knob3D}}, Path=Angle}"    
                Axis="0.0, 0.0, 1.0"/>
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
    </p:Knob3D.Transform>
    <p:Knob3D.Content>
        <ContainerUIElement3D>
            <p:Bar3D 
                SideCount="3" 
                RoundingRate="0.15"
                Material="{DynamicResource ControlMaterial}" />
        </ContainerUIElement3D>
    </p:Knob3D.Content>
</p:Knob3D>

Content customization

C# example :

In the C# code below, vMain represents the current Viewport.

private void GenerateCustomKnob()
{
    AxisAngleRotation3D rotation = new AxisAngleRotation3D();
    rotation.Axis = new Vector3D(0.0, 0.0, 1.0);
    RotateTransform3D rotateTransform = new RotateTransform3D();
    rotateTransform.Rotation = rotation;
    TranslateTransform3D translateTransform = new TranslateTransform3D(2.0, 3.0, 0.0);
    Transform3DGroup tg = new Transform3DGroup();
    tg.Children.Add(rotateTransform);
    tg.Children.Add(translateTransform);
    Perspective.Wpf3D.Shapes.Bar3D bar = new Perspective.Wpf3D.Shapes.Bar3D();
    bar.SideCount = 6;
    bar.RoundingRate = 0.15;
    Perspective.Wpf3D.Controls.Knob3D knob = new Perspective.Wpf3D.Controls.Knob3D();
    knob.OverridesDefaultSkin = true;
    knob.Transform = tg;
    knob.Content = bar;
    Binding b = new Binding("Angle");
    b.Source = knob;
    BindingOperations.SetBinding(rotation, AxisAngleRotation3D.AngleProperty, b);
    workshop.Children.Add(knob);
}

The 2 examples above are available in the CustomKnob3D.xaml page of the PerspectiveDemo.UI project. Online demo

Button3D

Button3D inherits from ButtonBase3D (which inherits from ContentControl3D).

Like the 2D ButtonBase class (namespace System.Windows.Controls.Primitives), ButtonBase3D exposes a Click event and an IsPressed property.

But ButtonBase3D also has a PressEffect property, whose value is automatically animated when the button is pressed or released. This animation can thus be reflected in the skin elements through databinding. That mitigates the absence of a FrameworkElement3D class which would offer dynamic styling and animation.

Fader3D and Knob3D

Fader3D and Knob3D inherit from RangeBase3D (which inherits from ContentControl3D).

RangeBase3D presents functionalities similar to those of the 2D RangeBase class (namespace System.Windows.Controls.Primitives). It makes it possible to manage an element having a value (Value property) inside a range (properties Minimum and Maximum).

RangeBase3D offers the following additional services:

  • Change of value by vertical mouse dragging (properties DragHeight and IsDragging), or by using the mouse wheel (MouseWheelIncrement property).
  • Enslaving of various controls of this family (like their 2D equivalents of the namespace Perspective.Wpf, Fader and Knob) : multi-selection by Ctrl-Click, IsSelected property. This may be very practical i.e. on a virtual mixer.

With the skins of Perspective, selected controls are highlighted. 3D Controls appear in intensified brightness (application of EmissiveMaterial) while 2D controls appear surrounded.

2D and 3D range controls multiselection

This example is available in the Mixer.xaml page of the PerspectiveDemo.UI project. Online demo

Skins architecture

The skins are stored as XAML files in the sources of the assembly, in the folder named Skins. This folder contains the common content for the different skins. The content specific to a skin is stored in a subfolder which has the name of the skin.

The skin mechanism tries to reproduce a system equivalent to 2D ControlTemplate of WPF. But styles, triggers and templates are not applicable to UIElement3D. Thus alternatives should have been found.

A skin contains 2 types of files :

  • Content files of the classes inherited from ContentControl3D (i.e. Knob3D.xaml), which contain a UIElement3D object automatically assigned to the Content property of the concerned class.
  • A Resource.xaml file containing a ResourceDictionary object, allowing to share objects common to different content files.

Each ContentControl3D automatically loads its visual from a content file specified in the resource named ContentFiles (of Hashtable type) defined in the Resources.xaml file of the current skin folder. Here the ContentFiles definition of the skin LaMoisson :

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"    
    xmlns:p3D="clr-namespace:Perspective.Wpf3D">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/Perspective.Wpf3D;component/Skins/Resources.xaml" />
    </ResourceDictionary.MergedDictionaries>
    <!--Content files : path relative to Skins directory - no .xaml extension-->
    <col:Hashtable x:Key="ContentFiles">
        <sys:String x:Key="Fader3D">LaMoisson/Fader3D</sys:String>
    </col:Hashtable>
    ...

Paths are relative to the Skins folder. The common content files (of the Skins folder) Button3D.xaml and Knob3D.xaml are automatically used by the skin LaMoisson. This avoids duplicating identical content files in different skin folders. But a specific content file, stored in the Skins/LaMoisson folder, is used for Fader3D.

ResourceDictionary of the Resources.xaml common file (of the Skins folder) is merged with the resources of the skin. Thus, a content file can refer to common resources, or to specific resources for the current skin.

Here is the content of Knob3D.xaml :

<ContainerUIElement3D
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:p3D="clr-namespace:Perspective.Wpf3D.Shapes"
    xmlns:p3DControls="clr-namespace:Perspective.Wpf3D.Controls">
    <ContainerUIElement3D.Transform >
        <RotateTransform3D >
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D 
                    Angle="{Binding RelativeSource={RelativeSource AncestorType={x:Type p3DControls:Knob3D}}, Path=Angle}"    
                    Axis="0.0, 0.0, 1.0"/>
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
    </ContainerUIElement3D.Transform>
    <ContainerUIElement3D Transform="{StaticResource KnobControlTransform}">
        <StaticResource ResourceKey="KnobControl"/>
        <!--Color part-->
        <p3D:Bar3D 
            SideCount="100" 
            Material="{DynamicResource ControlColorMaterial}" 
            Transform="{StaticResource ControlColorPartTransform}"/>
        <p3D:Bar3D 
            x:Name="PART_SelectionIndicator"
            SideCount="100" 
            Material="{DynamicResource SelectionIndicatorMaterial}" 
            Transform="{StaticResource ControlColorPartTransform}"/>
    </ContainerUIElement3D>
</ContainerUIElement3D>

The visual defined for Knob3D in this file is composed of an overlap of 2 ContainerUIElement3D, the 2nd being made up of an object loaded from the common resources, KnobControl, and of 2 Bar3D objects (color parts on the button front).

In 2D, the properties of the objects contained in a ControlTemplate can be related to those of the control via a TemplateBinding object. This mechanism is not available in 3D, but instead a RelativeSource Binding object can be used, here to bind the button rotation angle to its Angle property. The non-visual code of the control must thus expose properties for this purpose.

The code of a 2D control 2D can refer to parts of the template, which must thus exist in it with a given type and a given name. By convention, this name is prefixed by “PART_”. For example, the track of a Slider must be defined in the template and be called “PART_Track”. This convention is also used for the Perspective 3D skins.

In 2D, the optional TemplatePart attribute makes it possible at ends of documentation to associate the name and the type of the named part with the type of the control. in perspective, the SkinPart attribute plays the same role.

The FrameworkTemplate.FindName method makes it possible to find a named part to use it in the code of the control. The Perspective ContentControl3D class contains a FindName protected method which plays the same role within a skin.

Notes :

  • The named part must be defined in the control's content file, it is not possible to define it in a resource file.
  • The mechanism of the named parts is specific to skins. It is not usable when the Content property is assigned directly in the code of a window (the x:Name attribute having to be unique, there would be a problem with several control instanciations).

Here are the common resources used by Knob3D:

<ContainerUIElement3D 
    x:Key="KnobControl" 
    x:Shared="false">
    <p3D:Bar3D SideCount="100" Material="{DynamicResource ControlMaterial}" >
        <p3D:Bar3D.Transform>
            <ScaleTransform3D ScaleZ="0.2" />
        </p3D:Bar3D.Transform>
    </p3D:Bar3D>
    <p3D:Bar3D SideCount="100" Material="{DynamicResource ControlMaterial}">
        <p3D:Bar3D.Transform>
            <Transform3DGroup>
                <ScaleTransform3D ScaleX="0.8" ScaleY="0.8" ScaleZ="0.75" />
                <TranslateTransform3D OffsetZ="0.2" />
            </Transform3DGroup>
        </p3D:Bar3D.Transform>
    </p3D:Bar3D>
    <p3D:Box3D Material="{DynamicResource IndicatorMaterial}">
        <p3D:Box3D.Transform>
            <Transform3DGroup>
                <ScaleTransform3D ScaleX="0.1" ScaleY="0.5" ScaleZ="0.8"/>
                <TranslateTransform3D OffsetY="-1.0" OffsetZ="0.2"/>
            </Transform3DGroup>
        </p3D:Box3D.Transform>
    </p3D:Box3D>
</ContainerUIElement3D>
<Transform3DGroup x:Key="ControlColorPartTransform" >
    <ScaleTransform3D ScaleX="0.7" ScaleY="0.7" ScaleZ="0.1" />
    <TranslateTransform3D OffsetZ="0.95" />
</Transform3DGroup>
<ScaleTransform3D x:Key="KnobControlTransform"
    ScaleX="0.5" ScaleY="0.5" ScaleZ="0.5" />

Notice the use of the x:Shared="false" attribute to force the creation of a specific instance to each use of the resource, which is essential for the UIElement3D resources intended for the construction of the visual tree of controls.

The Material resources are described in the Resource.xaml skin specific file.

About this article

Author: Olivier Dewit.

History:

  • The 22th may, 2008 : update for the 0.9 version of Perspective.
  • December 16, 2007 : 1st publication (Perspective version 0.4). Original article