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;
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;
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;
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;
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;
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;
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.