Wednesday, June 10, 2009

3D projections in Silverlight 3 Beta – part 2

In my previous post on 3D projections in Silverlight 3 Beta, I created a basic PhotoCube that had images on the faces of a rotating cube created using the new 2D/3D PlaneProjection capabilities available in Silverlight 3.

BasicCube BasicPhotoCube

I thought I’d do a bit more and add some animation to the cube’s rotation and also do away with the slider and rotate the cube using the mouse. I thought it would be nice if the cube rotated slowly all the time, regardless of any mouse interaction, so the first thing to do was to set up an animation to do that. Because I want to be able to manipulate this animation whilst the various mouse events are firing to allow me to control of the cube’s rotation independent of the ‘background’ rotation, I need to define it at the page level:

private Storyboard slowStoryboard;
readonly Dictionary<Image, double> yRotations = new Dictionary<Image, double>();
private double mouseStart;
private bool mouseIsDown;

public PhotoCubePage()
{
InitializeComponent();

SetUpRotations();
slowStoryboard = CreateSlowRotation();
slowStoryboard.Begin();
}








private Storyboard CreateSlowRotation()
{
Storyboard newStoryboard = new Storyboard { Duration = new Duration(TimeSpan.FromSeconds(15)) };
newStoryboard.RepeatBehavior = RepeatBehavior.Forever;








    foreach (var imageRotation in yRotations)
{
var animation = new DoubleAnimation { Duration = new Duration(TimeSpan.FromSeconds(15)), BeginTime = new TimeSpan(0) };
newStoryboard.Children.Add(animation);
Storyboard.SetTarget(animation, (imageRotation.Key.Projection as PlaneProjection));
Storyboard.SetTargetProperty(animation, new PropertyPath("(PlaneProjection.RotationY)"));
animation.To = imageRotation.Value + 360;
}

return newStoryboard;
}








private void SetUpRotations()
{
foreach (Image image in ImageGrid.Children.Cast<Image>())
{
yRotations.Add(image, ((PlaneProjection)image.Projection).RotationY);
image.MouseLeftButtonDown += ImageMouseLeftButtonDown;
image.MouseLeftButtonUp += ImageMouseLeftButtonUp;
image.MouseMove += ImageMouseMove;
}
}











The animation is performed on each image as it will be for the mouse control, but a 15 second duration makes the cube turn slowly and I want a full rotation of 360 degrees. Setting the RepeatBehavior to Forever ensures that the animation doesn’t stop when it completes a complete rotation. The setup of the Dictionary of Images and their initial RotationY values has been moved to a method to tidy it up, and I now hook up the mouse events here as I need to respond to these on each image surface.









In the mouse down event I record the X-position of the mouse, pause the slow-moving animation and capture the mouse. In the mouse move event, I calculate the mouse movement in the Y-direction and do an rough calculation based on the width of a face of the cube and the amount of mouse movement, to decide on the animation of the faces of the cube which I feed into the cube animation method. In the mouse up event I release mouse capture and restart the slow rotation of the cube.









private void ImageMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mouseStart = e.GetPosition(this).X;
slowStoryboard.Pause();
mouseIsDown = ((UIElement)sender).CaptureMouse();
}

private void ImageMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
mouseIsDown = false;
ReleaseMouseCapture();
slowStoryboard = CreateSlowRotation();
slowStoryboard.Begin();
}

private void ImageMouseMove(object sender, MouseEventArgs e)
{
if (mouseIsDown)
{
double mouseMovement = mouseStart - e.GetPosition(this).X;
double rotation = mouseMovement / FrontImage.Width * 90;
AnimateRotation(rotation, 1);
}
}

private void AnimateRotation(double movement, double seconds)
{
var duration = new Duration(TimeSpan.FromSeconds(seconds));
var storyboard = new Storyboard { Duration = duration };

foreach (var imageRotation in yRotations)
{
var animation = new DoubleAnimation { Duration = duration, BeginTime = new TimeSpan(10) };
storyboard.Children.Add(animation);
Storyboard.SetTarget(animation, (imageRotation.Key.Projection as PlaneProjection));
Storyboard.SetTargetProperty(animation, new PropertyPath("(PlaneProjection.RotationY)"));
animation.To = ((PlaneProjection)imageRotation.Key.Projection).RotationX + imageRotation.Value + movement;
animation.EasingFunction = new ElasticEase { EasingMode = EasingMode.EaseOut, Oscillations = 3, Springiness = 6 };
}

storyboard.Begin();
}











Just so that all the code is shown, here is the fairly simple XAML for the page:









    <Grid x:Name="LayoutRoot" Background="DarkGray">
<
Grid x:Name="ImageGrid" >
<
Image x:Name="FrontImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/silverlight.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="0"/>
</
Image.Projection>
</
Image>
<
Image x:Name="LeftImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/OpenfeatureLizardSquare.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="90"/>
</
Image.Projection>
</
Image>
<
Image x:Name="BackImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/Waterfall.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="180"/>
</
Image.Projection>
</
Image>
<
Image x:Name="RightImage" Visibility="Visible" Width="154" Height="154" Stretch="Fill" Source="Images/Tulip.jpg">
<
Image.Projection>
<
PlaneProjection CenterOfRotationZ="-77" RotationY="270"/>
</
Image.Projection>
</
Image>
</
Grid>
</
Grid>











 









The result is that the cube turns slowly whilst the mouse is not being used to manipulate it and while the mouse is used to drag it the cube responds with animated movement; I use one of the pre-canned easing functions to give the animation a ‘springy’ feeling at the end of each movement. Notice that the animation method calculates the move-to value for each animation by adding the original position of the image/face to the movement value AND the current value of the RotationY which has been affected by the slow rotation.









And here’s the result:




















You may have noticed that although I have called the thing I am rotating a cube and in the first post there were actually six images, I have done away with the top and bottom of my cube in the latest mark-up. This is because they are not really visible whilst the ‘cube’ is rotating on just the Y-axis; I had intended to make it a proper cube and allow rotation along 2 axes (which would have given me full articulation of the cube, or even 3 – to allow me to move the cube in and out of the screen), but there is a problem when it comes to allowing movement in more that one direction.









The problem lies in the fact that once an image plane has been rotated along one axis, rotation in another one happens in relation to the original position, not the new one, so that the faces that become the sides of the cube, the back of the cube, the top and the bottom all need to be transformed in different ways to maintain the illusion of a solid shape in 3D space. For example, after the original Y-axis rotation to setup the cube’s sides and the X-axis rotation to setup the cube’s top and bottom, to rotate the cube along the X-axis, the sides need to be rotated along the Z-axis (i.e. the one that runs through their centre) and this assumes that they aren’t transformed further in the Y-axis themselves, by a slider or my attempts at animating them with mouse movement. In fact the maths for calculating the positions of the faces of the cube are quite complex; from dipping into Charles Petzold’s book: 3D Programming for Windows and quickly getting bogged down with quaternions and rotation matrices, it was clear the maths were beyond the scope of this post, so I chickened out and did away with the top and bottom of the cube and abandoned the notion of full articulation (for now). So we have ended up what might be better called a ‘PhotoPrism’ that rotates only along the Y-axis.









This new name did give me the idea for a further refinement of the cube: if the thing is a prism, there is no reason why it can’t have a variable number of faces, from 3 upwards – the maths for calculating the positioning and angles of rotation for the faces would be much easier.









Hmm, watch this space…