A quick look at Silverlight 3: Projections

One of the really cool things in Silverlight 3 is the ability to produce 3D effects using projections.

Whilst WPF has a full 3D system with meshes, materials, cameras, lights and so on – Silverlight 3 has a simpler system that allows you to create common 3D effects without having to delve off into all the complexities that come with 3D systems.

In Silverlight 3, the UIElement has a new property called Projection which is of type Projection – an abstract class with no members on it.

The only concrete form of Projection is the PlaneProjection which looks like this;

image 

so there’s only 12 additional properties to learn about but they add a lot in terms of graphical capability by allowing for new render transformations that can give the illusion of 3D by creating perspective views of content.

These are render transforms so they occur after items have gone through the layout system and can be combined with the existing transformations in Silverlight 2 like scale, skew, translate and rotate.

To think about these transformations, we need to put 3D axes onto the screen as in;

487px-3D_coordinate_system_svg

and then the rotation properties are fairly simple to understand in that setting in that if we have something like;

<UserControl
    x:Class="SilverlightApplication12.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid
        x:Name="LayoutRoot"
        Background="White">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid HorizontalAlignment="Center"
              VerticalAlignment="Center">
            <Grid.Projection>
                <PlaneProjection x:Name="projection" />
            </Grid.Projection>
            <Rectangle
                Stroke="Black"
                Fill="Lime"></Rectangle>
            <TextBlock
                Text="Text Block One"
                FontSize="36" />
        </Grid>
        <StackPanel Grid.Row="1">
            <StackPanel
                Orientation="Horizontal">
                <TextBlock
                    Text="Rotate X"
                    VerticalAlignment="Center"
                    Margin="5"/>
                <Slider
                    Value="{Binding ElementName=projection,Path=RotationX,Mode=TwoWay}"
                    Margin="10"
                    Minimum="0"
                    Maximum="360"
                    MinWidth="288" />               
            </StackPanel>
            <StackPanel
                Orientation="Horizontal">
                <TextBlock
                    Text="Rotate Y"
                    VerticalAlignment="Center"
                    Margin="5" />
                <Slider
                    Value="{Binding ElementName=projection,Path=RotationY,Mode=TwoWay}"
                    Margin="10"
                    Minimum="0"
                    Maximum="360"
                    MinWidth="288" />
            </StackPanel>
            <StackPanel
                Orientation="Horizontal">
                <TextBlock
                    Text="Rotate Z"
                    VerticalAlignment="Center"
                    Margin="5" />
                <Slider
                    Value="{Binding ElementName=projection,Path=RotationZ,Mode=TwoWay}"
                    Margin="10"
                    Minimum="0"
                    Maximum="360"
                    MinWidth="288" />
            </StackPanel>
        </StackPanel>       
    </Grid>
</UserControl>

then that presents a UI which I can then use to tweak with those rotation properties as in;

image

Now each of these properties is rotating the object around a CenterOfRotation.

For the X and Y co-ordinates, this centre of rotation is defined relative to the object itself in that the values range from 0 to 1 in both cases. For the Z co-ordinate, this CenterOfRotation is defined a global space.

So if I go ahead and duplicate my whole UI as in;

<UserControl
    x:Class="SilverlightApplication12.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid
        x:Name="LayoutRoot"
        Background="White">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition
                Height="Auto" />
        </Grid.RowDefinitions>
        <Grid
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
            <Grid.Projection>
                <PlaneProjection
                    x:Name="projection" />
            </Grid.Projection>
            <Rectangle
                Stroke="Black"
                Fill="Lime"></Rectangle>
            <TextBlock
                Text="Text Block One"
                FontSize="36" />
        </Grid>
        <Grid
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
            <Grid.Projection>
                <PlaneProjection
                    x:Name="projection2" />
            </Grid.Projection>
            <Rectangle
                Stroke="Black"
                Fill="Yellow"></Rectangle>
            <TextBlock
                Text="Text Block Two"
                FontSize="36" />
        </Grid>
        <StackPanel
            Orientation="Horizontal"
            Grid.Row="1">
            <StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Rotate X"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection,Path=RotationX,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="360"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Rotate Y"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection,Path=RotationY,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="360"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Rotate Z"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection,Path=RotationZ,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="360"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Centre X"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection,Path=CenterOfRotationX,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="1"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Centre Y"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection,Path=CenterOfRotationY,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="1"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Centre Z"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection,Path=CenterOfRotationZ,Mode=TwoWay}"
                        Margin="10"
                        Minimum="-192"
                        Maximum="192"
                        MinWidth="288" />
                </StackPanel>
            </StackPanel>
            <StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Rotate X"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection2,Path=RotationX,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="360"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Rotate Y"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection2,Path=RotationY,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="360"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Rotate Z"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection2,Path=RotationZ,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="360"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Centre X"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection2,Path=CenterOfRotationX,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="1"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Centre Y"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection2,Path=CenterOfRotationY,Mode=TwoWay}"
                        Margin="10"
                        Minimum="0"
                        Maximum="1"
                        MinWidth="288" />
                </StackPanel>
                <StackPanel
                    Orientation="Horizontal">
                    <TextBlock
                        Text="Centre Z"
                        VerticalAlignment="Center"
                        Margin="5" />
                    <Slider
                        Value="{Binding ElementName=projection2,Path=CenterOfRotationZ,Mode=TwoWay}"
                        Margin="10"
                        Minimum="-192"
                        Maximum="192"
                        MinWidth="288" />
                </StackPanel>
            </StackPanel>
        </StackPanel>
    </Grid>
</UserControl>

then that gives me a UI where I can do stuff like;

image image image

Hopefully that gives a static impression of what moving those centres of rotation around actually does.

Then there are the properties LocalOffsetX, LocalOffsetY, LocalOffsetZ – what these allow us to do is to offset the object after it has been rotated. The net effect is that the object is moved along vectors X’, Y’, Z’ which are perpendicular or normal to the rotated object itself. I hope that makes sense. As an example, if I have UI like this;

<UserControl x:Class="SilverlightApplication13.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <Grid x:Name="LayoutRoot" Background="White">

        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition
                Height="Auto" />           
        </Grid.RowDefinitions>
        <Grid
            x:Name="gridContent" />
        <Button
            Grid.Row="1"
            Margin="10"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            Content="Click"
            Click="OnClick"/>
    </Grid>
</UserControl>

with a bit of code like this;

  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
    }
    void OnClick(object sender, RoutedEventArgs args)
    {
      gridContent.Children.Clear();

      storyBoard = new Storyboard();
      storyBoard.Duration = new Duration(new TimeSpan(0, 0, 5));
      storyBoard.RepeatBehavior = RepeatBehavior.Forever;

      for (int i = 0; i < 6; i++)
      {
        Rectangle r = new Rectangle()
        {
          Width = 192,
          Height = 96,
          Fill = Colors[i],
          Projection = new PlaneProjection()
        };
        gridContent.Children.Add(r);

        DoubleAnimation offset = new DoubleAnimation()
        {
          To = 384,
          AutoReverse = true
        };
        Storyboard.SetTarget(offset, r.Projection);
        Storyboard.SetTargetProperty(offset, new PropertyPath("LocalOffsetZ"));
        storyBoard.Children.Add(offset);
        ((PlaneProjection)r.Projection).RotationY = (360.0 / 6.0) * i + 20.0;
      }
      storyBoard.Begin();
    }
    Storyboard storyBoard;

    static Brush[] Colors = {
      new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0x00, 0x00)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0xFF, 0x00)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0x00, 0xFF)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0xFF, 0x00)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0xFF, 0xFF)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0x00, 0xFF))
    };
  }

then the code is drawing 6 rectangles in the same location, it then rotates each one of them around the Y axis and animates their LocalOffsetZ direction. The key is that the LocalOffsetZ direction is POST-rotation rather than pre-rotation so we get an effect like this;

image image image

showing (hopefully) that the offset along the Z axis is an offset along an axis perpendicular to each rectangle itself POST rotation.

The remaining three properties are the GlobalOffsetX,Y,Z properties – these work similarly to the LocalOffset properties except that the vectors that these are the standard X,Y, Z directions – i.e. they are not rotated. Re-working that previous code;

  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();
    }
    void OnClick(object sender, RoutedEventArgs args)
    {
      gridContent.Children.Clear();

      storyBoard = new Storyboard();
      storyBoard.Duration = new Duration(new TimeSpan(0, 0, 5));
      
      int offsetZ = -192;
      int offsetX = -192;

      for (int i = 0; i < 6; i++)
      {
        Rectangle r = new Rectangle()
        {
          Width = 192,
          Height = 96,
          Fill = Colors[i],
          Projection = new PlaneProjection()
        };               

        gridContent.Children.Add(r);

        DoubleAnimation daRotationY = new DoubleAnimation()
        {
          To = -45
        };
        Storyboard.SetTarget(daRotationY, r.Projection);
        Storyboard.SetTargetProperty(daRotationY, new PropertyPath("RotationY"));
        storyBoard.Children.Add(daRotationY);

        DoubleAnimation daOffsetX = new DoubleAnimation()
        {
          To = offsetX
        };
        Storyboard.SetTarget(daOffsetX, r.Projection);
        Storyboard.SetTargetProperty(daOffsetX, new PropertyPath("GlobalOffsetX"));
        storyBoard.Children.Add(daOffsetX);
        offsetX += 64;

        DoubleAnimation daOffsetZ = new DoubleAnimation()
        {
          To = offsetZ
        };
        Storyboard.SetTarget(daOffsetZ, r.Projection);
        Storyboard.SetTargetProperty(daOffsetZ, new PropertyPath("GlobalOffsetZ"));
        storyBoard.Children.Add(daOffsetZ);
        offsetZ += 64;
      }
      storyBoard.Begin();
    }
    Storyboard storyBoard;

    static Brush[] Colors = {
      new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0x00, 0x00)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0xFF, 0x00)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0x00, 0xFF)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0xFF, 0x00)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0x00, 0xFF, 0xFF)),
      new SolidColorBrush(Color.FromArgb(0xFF, 0xFF, 0x00, 0xFF))
    };
  }

then gives us an effect as below;

image

where each rectangle is offset to the right of the previous rectangle and further “forward” along the Z axis and, in both cases, that’s the “standard definition” of the X,Z axes rather than axes that have been rotated as in the case with the local offset example.

Enjoy!

You can download the code for this post from here.

This is one of a series of posts taking a quick look at Silverlight 3 – expect them to be a little “rough and ready” and that they’ll get expanded on as I’ve had more time to work with Silverlight 3.