Showing posts with label tutorial. Show all posts
Showing posts with label tutorial. Show all posts

Thursday, June 24, 2010

WPF Cover Flow Tutorial : Part 8

I've been told that Part 7 had memory leaks. In the comments, I link a blog article describing how to study memory leaks : you can watch the Private Bytes performance counter.

We need :
  • The performance counter
  • A callback displaying the counter value
  • A timer
using System.Diagnostics;
using System.Windows.Threading;

private readonly PerformanceCounter counter = GetCounter();
private readonly DispatcherTimer timer = new DispatcherTimer();

private void timer_Tick(object sender, EventArgs e)
{
long kb = Convert.ToInt64(counter.NextValue() / 1000);
perfLabel.Content = string.Format("{0,12} KB", kb.ToString("###.###.###"));
}
private static PerformanceCounter GetCounter()
{
var counter = new PerformanceCounter();
counter.CategoryName = "Process";
counter.CounterName = "Private Bytes";
counter.InstanceName = Process.GetCurrentProcess().ProcessName;
return counter;
}
public TestWindow()
{
...
timer.Tick += timer_Tick;
timer.Interval = TimeSpan.FromMilliseconds(100);
timer.Start();
}

<Grid>
...
<Label Content="0 KB" Height="23" HorizontalAlignment="Left" Margin="12,12,0,0" Name="perfLabel" VerticalAlignment="Top" Width="142" Foreground="Red" />
</Grid>
Here is the result in video.

Current implementation always stay close to 90 KB :


If we comment out Cover Destroy method:
public void Destroy()
{
//visualModel.Children.Remove(this);
}
The counter grows continuously :



Note : the ThumbnailManager does not handle empty image files. Loading such a file would throw an OutOfMemoryException.
Edit 2014-02-23 : Code has moved to github.

Monday, April 20, 2009

WPF Cover Flow Tutorial : Part 7 (source)

Before disclosing sources of Part 7, here a few notes.

I provide a sample ThumbnailManager, working with an IsolatedStorageFile.
public class ThumbnailManager : IThumbnailManager
{
#region Fields
private readonly IsolatedStorageFile store;
#endregion
public ThumbnailManager()
{
store = IsolatedStorageFile.GetUserStoreForAssembly();
}
public ImageSource GetThumbnail(string host, string path)
{
string thumbName = Path.GetFileName(path);
if (store.GetFileNames(thumbName).Length == 0)
{
using (var stream = new IsolatedStorageFileStream(thumbName, FileMode.CreateNew, store))
{
byte[] data = GetThumbnail(path);
stream.Write(data, 0, data.Length);
}
}
using (var stream = new IsolatedStorageFileStream(thumbName, FileMode.Open, store))
{
var image = new BitmapImage();
image.BeginInit();
image.CacheOption = BitmapCacheOption.OnLoad;
image.StreamSource = stream;
image.EndInit();
image.Freeze();
return image;
}
}
}
I deal with host names because I am also working on an implementation dealing with many shares on the network. The GetThumbnail method just resizes pictures.
    private byte[] GetThumbnail(string path)
{
Image source = Image.FromFile(path);
source = AmazonCut(source);
int height = source.Height;
int width = source.Width;
int factor = (height - 1) / 250 + 1;
int smallHeight = height / factor;
int smallWidth = width / factor;
Image thumb = source.GetThumbnailImage(smallWidth, smallHeight, null, IntPtr.Zero);
using (var ms = new MemoryStream())
{
thumb.Save(ms, ImageFormat.Png);
ms.Flush();
ms.Seek(0, SeekOrigin.Begin);
var result = new byte[ms.Length];
ms.Read(result, 0, (int)ms.Length);
return result;
}
}
For the sample videos on youtube, I downloaded many covers from amazon. That's why I need the helper function that removes the blank frame around the picture:
    private static Image AmazonCut(Image image)
{
if (image.Width != image.Height)
return image;
var bmp = new Bitmap(image);
int size = image.Height;
int white = System.Drawing.Color.FromKnownColor(KnownColor.White).ToArgb();
int i = 0;
while (i < size / 2)
{
if (bmp.GetPixel(i, i).ToArgb() != white)
break;
if (bmp.GetPixel(i, size - 1 - i).ToArgb() != white)
break;
if (bmp.GetPixel(size - 1 - i, i).ToArgb() != white)
break;
if (bmp.GetPixel(size - 1 - i, size - 1 - i).ToArgb() != white)
break;
i++;
}
if (i > 0)
{
i += 8;
var zone = new Rectangle(i, i, size - 2 * i, size - 2 * i);
return bmp.Clone(zone, System.Drawing.Imaging.PixelFormat.DontCare);
}
return bmp;
}
Well this whole class is not perfect code, but it is sufficient for a demo.

If you do not know innerings of IsolatedStorage, you will find the thumbnails in a folder like C:\Documents and Settings\ded\Local Settings\Application Data\IsolatedStorage\0ypqvhod.rll\j4pydd3v.g4v\StrongName.alr3sbzvfezz2fk22sd5g0b5dxbwzr0b\AssemFiles.

Edit 2014-02-23 : Code has moved to github.

Wednesday, April 15, 2009

WPF Cover Flow Tutorial : Part 7

After some improvements in the Cover class, let's describe how virtualization works.

We do not keep all covers in memory. Remember the drawing in Part 2. At a given time, we only keep a few covers on both sides of the current cover (at position index). This means that every time we browse through the covers, we create and destroy one or more covers. If we do not keep all covers in memory, we have to keep enough information to rebuild covers : image file path and position. We also need to remember which covers are currently built.
public const int HalfRealizedCount = 6;
public const int PageSize = HalfRealizedCount;
private readonly ICoverFactory coverFactory;
private readonly Dictionary<int, ImageInfo> imageList = new Dictionary<int, ImageInfo>();
private readonly Dictionary<int, ICover> coverList = new Dictionary<int, ICover>();
private int index;
private int firstRealized = -1;
private int lastRealized = -1;
  • HalfRealizedCount is the number of covers created before and after current cover
  • I use the same value for the PageSize (we could choose a different value) when we will move by more than one cover at a time
  • imageList will map cover positions to image paths
  • coverList will be used as an array to keep realized covers
  • firstRealized and lastRealized are the positions of respectively first and last realized covers
There will be two ways to browse covers : by less than a page with animation or jumping by more than a page without animation.
    private void RotateCover(int pos, bool animate)
{
if (coverList.ContainsKey(pos))
coverList[pos].Animate(index, animate);
}
private void UpdateIndex(int newIndex)
{
if (index != newIndex)
{
bool animate = Math.Abs(newIndex - index) < PageSize;
UpdateRange(newIndex);
int oldIndex = index;
index = newIndex;
if (index > oldIndex)
{
if (oldIndex < firstRealized)
oldIndex = firstRealized;
for (int i = oldIndex; i <= index; i++)
RotateCover(i, animate);
}
else
{
if (oldIndex > lastRealized)
oldIndex = lastRealized;
for (int i = oldIndex; i >= index; i--)
RotateCover(i, animate);
}
camera.Position = new Point3D(Cover.CoverStep * index, camera.Position.Y, camera.Position.Z);
}
}
I'm not sure if the test in RotateCover is still needed. Here is how the new UpdateIndex function. We compute the animation boolean like described before. The UpdateRange function deals with cover realization : it will create and destroy covers as necessary and updates both realized indexes. Then, for all realized covers between old and new index, we rotate the covers. Finally, like before, we update camera positions.
    private void RemoveCover(int pos)
{
if (!coverList.ContainsKey(pos))
return;
coverList[pos].Destroy();
coverList.Remove(pos);
}
private void UpdateRange(int newIndex)
{
int newFirstRealized = Math.Max(newIndex - HalfRealizedCount, 0);
int newLastRealized = Math.Min(newIndex + HalfRealizedCount, imageList.Count - 1);
if (lastRealized < newFirstRealized || firstRealized > newLastRealized)
{
visualModel.Children.Clear();
coverList.Clear();
}
else if (firstRealized < newFirstRealized)
{
for (int i = firstRealized; i < newFirstRealized; i++)
RemoveCover(i);
}
else if (newLastRealized < lastRealized)
{
for (int i = lastRealized; i > newLastRealized; i--)
RemoveCover(i);
}
for (int i = newFirstRealized; i <= newLastRealized; i++)
{
if (!coverList.ContainsKey(i))
{
ICover cover = coverFactory.NewCover(imageList[i].Host, imageList[i].Path, i, newIndex);
coverList.Add(i, cover);
}
}
firstRealized = newFirstRealized;
lastRealized = newLastRealized;
}
In the UpdateRange method, we first compute realized indexes. Then, we remove necessary covers (might need all covers if we move by more than HalfRealizedCount. Finally, we create missing covers.
    public void GoToNext()
{
UpdateIndex(Math.Min(index + 1, imageList.Count - 1));
}
public void GoToPrevious()
{
UpdateIndex(Math.Max(index - 1, 0));
}
public void GoToNextPage()
{
UpdateIndex(Math.Min(index + PageSize, imageList.Count - 1));
}
public void GoToPreviousPage()
{
UpdateIndex(Math.Max(index - PageSize, 0));
}
Navigation methods are self explanatory.

Possible improvements :
  • Slider
  • Events
  • Tags (e.g. album names to jump to covers)
Source is available in next post.

Tuesday, April 14, 2009

WPF Cover Flow Tutorial : Part 6 (bis)

I'll start with some Cover refactoring :
  • Class becomes internal
  • I add the ICover interface, mainly for unit testing with a fake class implementing ICover :
    public interface ICover
    {
    void Animate(int index, bool animate);
    bool Matches(MeshGeometry3D mesh);
    void Destroy();
    }
  • You may notice the new Destroy method. This is useful to be able to create or destroy objects at will. Relatively, covers now know about the containing ModelVisual3D.
  • I also add a static IThumbnailManager to put elsewhere all the code dealing with thumbnails (like managing an IsolatedStorageFile for example)
  • Animation is slightly modified : in some cases, no rotation is needed to allow faster browsing.
Here are the changes since Part 5 :
public interface IThumbnailManager
{
ImageSource GetThumbnail(string host, string path);
}
internal class Cover : ModelVisual3D, ICover
{
...
private static IThumbnailManager thumbCache;
private readonly ModelVisual3D visualModel;
...
private readonly string imageName;
...
private static ImageSource LoadImageSource(ImageInfo info)
{
if (thumbCache == null)
throw new InvalidOperationException("No thumbnail cache.");
return thumbCache.GetThumbnail(info.Host, info.Path);
}
...
public Cover(ImageInfo info, int coverPos, int currentPos, ModelVisual3D model)
{
pos = coverPos;
imageName = new FileInfo(info.Path).Name;
visualModel = model;

imageSource = LoadImageSource(info);
modelGroup = new Model3DGroup();
modelGroup.Children.Add(new GeometryModel3D(Tessellate(), LoadImage(imageSource)));
modelGroup.Children.Add(new GeometryModel3D(TessellateMirror(), LoadImageMirror(imageSource)));

rotation = new AxisAngleRotation3D(new Vector3D(0, 1, 0), RotationAngle(currentPos));
translation = new TranslateTransform3D(TranslationX(currentPos), 0, TranslationZ(currentPos));
var transformGroup = new Transform3DGroup();
transformGroup.Children.Add(new RotateTransform3D(rotation));
transformGroup.Children.Add(translation);
modelGroup.Transform = transformGroup;

Content = modelGroup;

visualModel.Children.Add(this);
}
public static IThumbnailManager Cache
{
set { thumbCache = value; }
}
public void Animate(int index, bool animate)
{
if (animate || rotation.HasAnimatedProperties)
{
var rotateAnimation = new DoubleAnimation(RotationAngle(index), AnimationDuration);
var xAnimation = new DoubleAnimation(TranslationX(index), AnimationDuration);
var zAnimation = new DoubleAnimation(TranslationZ(index), AnimationDuration);
rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, rotateAnimation);
translation.BeginAnimation(TranslateTransform3D.OffsetXProperty, xAnimation);
translation.BeginAnimation(TranslateTransform3D.OffsetZProperty, zAnimation);
}
else
{
rotation.Angle = RotationAngle(index);
translation.OffsetX = TranslationX(index);
translation.OffsetZ = TranslationZ(index);
}
}
public void Destroy()
{
visualModel.Children.Remove(this);
}
public override string ToString()
{
return string.Format("{0} {1}", pos, imageName);
}
}
I also add an ICoverFactory (mainly for testing purposes) :
public interface ICoverFactory
{
ICover NewCover(string host, string path, int coverPos, int currentPos);
}
internal class ImageInfo
{
public ImageInfo(string host, string path)
{
Host = host;
Path = path;
}
public string Host { get; private set; }
public string Path { get; private set; }
}
internal class CoverFactory : ICoverFactory
{
private readonly ModelVisual3D visualModel;
public CoverFactory(ModelVisual3D visualModel)
{
this.visualModel = visualModel;
}
#region ICoverFactory Members
public ICover NewCover(string host, string path, int coverPos, int currentPos)
{
return new Cover(new ImageInfo(host, path), coverPos, currentPos, visualModel);
}
#endregion
}
The FlowControl class is greatly refactored to implement virtualization.

WPF Cover Flow Tutorial : 6 months intermission

Well, sorry guys for not posting since October... I didn't have enough time to go on this tutorial. Less time available for coding and I've been working on a wider project where my Flow assembly was just a small GUI component.

There will be quite a jump in the source code. But I'll try to describe what is the main idea behind virtualization...

Coming soon !

Friday, October 24, 2008

WPF Cover Flow Tutorial : Part 6

After building a standalone component in Part 5, I wanted to improve performance.

I've read many blogs about IScrollInfo, ItemsContainerGenerator, Generator, Selector and ItemsControl classes. I also digged into WPF source code through Reflector. But eventually I realized that this was all about 2D. And here, we are dealing with 3D. Therefore, we have to create our own solution...

To be continued... here.

Saturday, October 18, 2008

WPF Cover Flow Tutorial : Part 5

After Part 4, we will partly refactor the code to build a custom component.

We define a new assembly called FlowComponent where we will move most of the code. First, the c# code will be part of a new class called FlowControl. This class herits from UserControl :
using[...]
namespace Ded.Tutorial.Wpf.CoverFlow.Part5.FlowComponent
{
public partial class FlowControl : UserControl
{
#region Fields
private int index;
private readonly List<Cover> coverList = new List<Cover>();
#endregion
#region Private stuff
private void RotateCover(int pos)[...]
private void UpdateIndex(int newIndex)[...]
private void viewPort_MouseDown(object sender, MouseButtonEventArgs e)[...]
#endregion
public FlowControl()
{
InitializeComponent();
}
public void Load(string imagePath)
{
coverList.Clear();
var imageDir = new DirectoryInfo(imagePath);
int doneImages = 0;
foreach (FileInfo image in imageDir.GetFiles("*.jpg"))
{
var cover = new Cover(image.FullName, doneImages++);
coverList.Add(cover);
visualModel.Children.Add(cover);
}
}
public void GoToNext()
{
if (index < coverList.Count - 1)
UpdateIndex(index + 1);
}
public void GoToPrevious()
{
if (index > 0)
UpdateIndex(index - 1);
}
}
}
The code from the TestWindow constructor is included in a new method called Load. It will build covers with the images from one directory. We also provide two new methods GoToNext and GoToPrevious to navigate between covers.

This is the same in the xaml code : we migrate the Grid (its background) and the Viewport3D to our custom UserControl :
<UserControl x:Class="Ded.Tutorial.Wpf.CoverFlow.Part5.FlowComponent.FlowControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="282" Width="490">
<Grid ClipToBounds="True">
<Grid.Background[...]>
<Viewport3D x:Name="viewPort" Grid.Column="0" Grid.Row="0" ClipToBounds="False" MouseDown="viewPort_MouseDown">
<Viewport3D.Camera[...]>
<Viewport3D.Children[...]>
</Viewport3D>
</Grid>
</UserControl>
We can set the size we want. It will be overriden in the owner component. We set the ClipToBounds property to True to avoid covers from exceeding the bounds of our component.

The Cover class is also moved to the FlowComponent assembly.

As we are creating a CoverFlow component, it is a good idea to deal with rectangle images. We update the Point3D coordinates with the ImageSource size. The ImageSource will be stored as a class attribute.
private readonly ImageSource imageSource;
private double RectangleDx()
{
if (imageSource.Width > imageSource.Height)
return 0;
else
return 1 - imageSource.Width / imageSource.Height;
}
private double RectangleDy()
{
if (imageSource.Width > imageSource.Height)
return 1 - imageSource.Height / imageSource.Width;
else
return 0;
}
private Geometry3D Tessellate()
{
double dx = RectangleDx();
double dy = RectangleDy();
var p0 = new Point3D(-1 + dx, -1 + dy, 0);
var p1 = new Point3D(1 - dx, -1 + dy, 0);
var p2 = new Point3D(1 - dx, 1 - dy, 0);
var p3 = new Point3D(-1 + dx, 1 - dy, 0);
...
}
private Geometry3D TessellateMirror()
{
double dx = RectangleDx();
double dy = RectangleDy();
var p0 = new Point3D(-1 + dx, -3 + 3 * dy, 0);
var p1 = new Point3D(1 - dx, -3 + 3 * dy, 0);
var p2 = new Point3D(1 - dx, -1 + dy, 0);
var p3 = new Point3D(-1 + dx, -1 + dy, 0);
...
}
public Cover(string imagePath, int pos)
{
...
imageSource = LoadImageSource(imagePath);
...
}
With this new improvement, it is possible to use the component to browse photos :
There is nothing much left in the TestWindow class. In the xaml code, after we've referenced our component assembly, we just use our FlowControl as main Content :
<Window x:Class="Ded.Tutorial.Wpf.CoverFlow.Part5.TestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Coverflow" Width="512" Height="320" KeyDown="Window_KeyDown"
xmlns:flow="clr-namespace:Ded.Tutorial.Wpf.CoverFlow.Part5.FlowComponent;assembly=Ded.Tutorial.Wpf.CoverFlow.Part5.FlowComponent">
<flow:FlowControl x:Name="flow" Margin="0"></flow:FlowControl>
</Window>
In the C# code, we just keep the Window_KeyDown method and the constructor :
using System.Windows;
using System.Windows.Input;
namespace Ded.Tutorial.Wpf.CoverFlow.Part5
{
public partial class TestWindow : Window
{
#region Private stuff
private void Window_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Right:
flow.GoToNext();
break;
case Key.Left:
flow.GoToPrevious();
break;
}
}
#endregion
public TestWindow()
{
InitializeComponent();
flow.Load(@"c:\_covers");
}
}
}
That's it ! We've built a CoverFlow component reusable in other WPF applications. But this component needs improvements. We will add virtualization in the next part...
Continue with Part 6.
Edit 2014-02-23 : Code has moved to github.

Friday, October 17, 2008

WPF Cover Flow Tutorial : Part 4

We can improve the HCI from Part 3 with some mouse handling.

We add a MouseDown handler method to our Viewport3D :
private void OnViewportMouseDown(object sender, MouseButtonEventArgs e)
{
var rayMeshResult = (RayMeshGeometry3DHitTestResult)VisualTreeHelper.HitTest(viewPort, e.GetPosition(viewPort));
if (rayMeshResult != null)
{
for (int i = 0; i < coverList.Count; i++)
{
if (coverList[i].Matches(rayMeshResult.MeshHit))
{
UpdateIndex(i);
break;
}
}
}
}
This method uses the VisualTreeHelper to find which mesh was hitten by the mouse click. If there is a match, we update the index like we did before. Matches are found with this new Cover method :
public bool Matches(MeshGeometry3D mesh)
{
foreach (GeometryModel3D part in modelGroup.Children)
if (part.Geometry == mesh)
return true;
return false;
}
We also modify the TestWindow constructor to load all the pictures from a sample folder :
public TestWindow()
{
InitializeComponent();
var imageDir = new DirectoryInfo(@"c:\_covers");
int doneImages = 0;
foreach (FileInfo image in imageDir.GetFiles("*.jpg"))
{
var cover = new Cover(image.FullName, doneImages++);
coverList.Add(cover);
visualModel.Children.Add(cover);
}
}
Mouse click may increment or decrement the index by more than one cover. That's wy we need to fix the UpdateIndex method :
private void UpdateIndex(int newIndex)
{
if (index != newIndex)
{
int oldIndex = index;
index = newIndex;
if (index > oldIndex)
for (int i = oldIndex; i <= index; i++)
RotateCover(i);
else
for (int i = oldIndex; i >= index; i--)
RotateCover(i);
camera.Position = new Point3D(.2 * index, camera.Position.Y, camera.Position.Z);
}
}
It is now time to play with more covers. Let's try the current application with a sample folder containing 500+ covers. These covers are usually between 400x400 and 500x500 pixels. The next Process Explorer capture shows that the application needs a lot of cpu and memory resources :
  • Loading all covers brings the cpu to 100%.
  • Browing all covers with the Right key pressed also needs a lot of cpu. Moreover, this rises the cache size from 300 MB to more than 1 GB.
It is possible to improve performance if we use thumbnails for all covers. We will store thumbnails in a subfolder.
private ImageSource LoadImageSource(string imagePath)
{
var imageFile = new FileInfo(imagePath);
var thumbnailDir = new DirectoryInfo(Path.Combine(imageFile.Directory.FullName, "tn"));
if (!thumbnailDir.Exists)
thumbnailDir.Create();
var thumbnail = new FileInfo(Path.Combine(thumbnailDir.FullName, imageFile.Name));
if (!thumbnail.Exists)
{
Image source = Image.FromFile(imageFile.FullName);
int height = source.Height;
int width = source.Width;
int factor = (height - 1) / 250 + 1;
int smallHeight = height / factor;
int smallWidth = width / factor;
Image thumb = source.GetThumbnailImage(smallWidth, smallHeight, null, IntPtr.Zero);
thumb.Save(thumbnail.FullName);
}
return new BitmapImage(new Uri(thumbnail.FullName, UriKind.RelativeOrAbsolute));
}
Thumbnails improve performance while browsing, but it is still slow at startup. We will try to improve this later.

Continue with Part 5.
Edit 2014-02-23 : Code has moved to github.

Thursday, October 16, 2008

WPF Cover Flow Tutorial : Part 3

In Part 2, we miss the animation part of the flow.

In the main TestWindow, we save all the covers in a List :
private readonly List<Cover> coverList = new List<Cover>();
public TestWindow()
{
InitializeComponent();
var assembly = new FileInfo(Assembly.GetExecutingAssembly().Location);
var image = new FileInfo(Path.Combine(assembly.Directory.FullName, "Katy Perry.jpg"));
for (int i = 0; i < 10; i++)
{
var cover = new Cover(image.FullName, i);
coverList.Add(cover);
visualModel.Children.Add(cover);
}
}
We add an handler for the KeyDown event on the Window element. We will only deal with the Right and Left keys. Once one of these keys is pressed down, we animate the old current cover and the new one.
private void RotateCover(int pos)
{
coverList[pos].Animate(index);
}
private void UpdateIndex(int newIndex)
{
if (index != newIndex)
{
int oldIndex = index;
index = newIndex;
RotateCover(oldIndex);
RotateCover(index);
camera.Position = new Point3D(.2 * index, camera.Position.Y, camera.Position.Z);
}
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
int newIndex = index;
switch (e.Key)
{
case Key.Right:
if (newIndex < coverList.Count - 1)
newIndex++;
break;
case Key.Left:
if (newIndex > 0)
newIndex--;
break;
}
UpdateIndex(newIndex);
}
Currently, we do not animate covers. We just move them from one place to another. In order to get a real animation, we have to deal with Animation objects. In this new version of the Animate method, we ask the engine to animate the covers. As we have saved the translation and rotation objects in two Cover attributes, we can directly update their parameters (angle and offsets).
public void Animate(int index)
{
TimeSpan duration = TimeSpan.FromMilliseconds(500);
var rotateAnimation = new DoubleAnimation(RotationAngle(index), duration);
var xAnimation = new DoubleAnimation(TranslationX(index), duration);
var zAnimation = new DoubleAnimation(TranslationZ(index), duration);
rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, rotateAnimation);
translation.BeginAnimation(TranslateTransform3D.OffsetXProperty, xAnimation);
translation.BeginAnimation(TranslateTransform3D.OffsetZProperty, zAnimation);
}
. Continue with Part 4.
Edit 2014-02-23 : Code has moved to github.

WPF Cover Flow Tutorial : Part 2

Since Part 1, we now know how to display a cover in 3D.

In order to display a reflection mirror, we add another GeometryModel3D to our Model3DGroup. This new element will be based on these points :
using Point2D = System.Windows.Point;
private Geometry3D TessellateMirror()
{
var p0 = new Point3D(-1, -3, 0);
var p1 = new Point3D(1, -3, 0);
var p2 = new Point3D(1, -1, 0);
var p3 = new Point3D(-1, -1, 0);
var q0 = new Point2D(0, 1);
var q1 = new Point2D(1, 1);
var q2 = new Point2D(1, 0);
var q3 = new Point2D(0, 0);
return BuildMesh(p0, p1, p2, p3, q0, q1, q2, q3);
}
We define the Point2D alias because of the conflict with System.Drawing.Point.

For the texture, we use a VisualBrush object and we apply a LinearGradientBrush to the image OpacityMask :
using MediaColor = System.Windows.Media.Color;
private Material LoadImageMirror(ImageSource imSrc)
{
var image = new System.Windows.Controls.Image();
image.Source = imSrc;
MediaColor startColor = MediaColor.FromArgb(127, 255, 255, 255);
MediaColor endColor = MediaColor.FromArgb(127, 255, 255, 255);
image.OpacityMask = new LinearGradientBrush(startColor, endColor, 90.0);
var brush = new VisualBrush(image);
return new DiffuseMaterial(brush);
}

There are two possibilities to browse the covers :
  • Keep the camera at the very same place and translate/rotate all covers as we are browsing.
  • Transform only a few covers (e.g. the one currently selected) and move the camera.
The second solution will ensure better performance.The current cover will be put in front. The other covers will be rotated and placed on each side of the current cover. When the camera will move, the current cover will be moved back with the unselected ones, and the newly selected cover will be put in front.

There are two transformations : a rotation and a translation on X and Z axes. The rotation angle will be 90, 0 or -90 degrees. The Z translation will be 0 or 1 for the current cover. The X translation will be 0 for the current cover, or proportional to the distance from the origin. For each cover, we will save its position. We will compare the cover position with the current index to compute the transformation parameters. This gives us :
private readonly int pos;
private double RotationAngle(int index)
{
return Math.Sign(pos - index) * -90;
}
private double TranslationX(int index)
{
return pos * .2 + Math.Sign(pos - index) * 1.6;
}
private double TranslationZ(int index)
{
return pos == index ? 1 : 0;
}
Applying the transformations to our Model3DGroup is easy. We create the transformation in the constructor with a default index of 0 :
private readonly AxisAngleRotation3D rotation;
private readonly TranslateTransform3D translation;
public Cover(string imagePath, int pos)
{
this.imagePath = imagePath;
this.pos = pos;

ImageSource imSrc = LoadImageSource();
modelGroup = new Model3DGroup();
modelGroup.Children.Add(new GeometryModel3D(Tessellate(), LoadImage(imSrc)));
modelGroup.Children.Add(new GeometryModel3D(TessellateMirror(), LoadImageMirror(imSrc)));

rotation = new AxisAngleRotation3D(new Vector3D(0, 1, 0), RotationAngle(0));
translation = new TranslateTransform3D(TranslationX(0), 0, TranslationZ(0));
var transformGroup = new Transform3DGroup();
transformGroup.Children.Add(new RotateTransform3D(rotation));
transformGroup.Children.Add(translation);
modelGroup.Transform = transformGroup;
Content = modelGroup;
}
Then, we will call the Animate method the transform the cover :
public void Animate(int index)
{
rotation.Angle = RotationAngle(index);
translation.OffsetX = TranslationX(index);
translation.OffsetZ = TranslationZ(index);
}
If we create three covers, we realize that only one is visible :We need to replace our single light source with two different ones :

<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="White" Direction="1,0,-3" />
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="White" Direction="-1,0,-3" />
</ModelVisual3D.Content>
</ModelVisual3D>
Continue with Part 3.
Edit 2014-02-23 : Code has moved to github.

Tuesday, October 14, 2008

WPF Cover Flow Tutorial : Result

Here is a sample video that shows the power of WPF : These poor videos were done with Windows Media Encoder. CamStudio actually performs much more better. I will use the latter for the next screencasts.

Monday, October 13, 2008

WPF Cover Flow Tutorial : Part 1

Disclaimer : if you don't know anything about WPF, you should read this excellent tutorial. This should be considered as Part 0.

Now, I will describe how to develop a Cover Flow component in WPF.
The z-axis is not visible here. Actually, it is going in your direction.

The first basic task will just display one cover in the middle of the screen.
In 3D, we usually work with triangles.
So we simply cut the square in two :

Let's start with the code. We create the 4 points :
var p0 = new Point3D(-1, -1, 0);
var p1 = new Point3D(1, -1, 0);
var p2 = new Point3D(1, 1, 0);
var p3 = new Point3D(-1, 1, 0);
Then we create a MeshGeometry3D object. This object will contain our model. In order to build a model, we need to set all the points. Then, for each triangle, we define the point indices and the normals. To calculate the normals, I use the same method as the one defined in Part 0.
private Vector3D CalculateNormal(Point3D p0, Point3D p1, Point3D p2)
{
  var v0 = new Vector3D(p1.X - p0.X, p1.Y - p0.Y, p1.Z - p0.Z);
  var v1 = new Vector3D(p2.X - p1.X, p2.Y - p1.Y, p2.Z - p1.Z);
  return Vector3D.CrossProduct(v0, v1);
}
For the first triangle, we take the first 3 points. The normal is OK with these points (going towards us, in the positive direction of the z-axis). We need to pay attention to the second triangle. If we simply take the points in the same order (e.g. 1, 2 and 3), the normal will be inverted, in the negative direction of the z-axis. So we choose the points 0, 2 and 3.
var mesh = new MeshGeometry3D();
mesh.Positions.Add(p0);
mesh.Positions.Add(p1);
mesh.Positions.Add(p2);
mesh.Positions.Add(p3);

var normal = CalculateNormal(p0, p1, p2);
mesh.TriangleIndices.Add(0);
mesh.TriangleIndices.Add(1);
mesh.TriangleIndices.Add(2);
mesh.Normals.Add(normal);

normal = CalculateNormal(p2, p3, p0);
mesh.TriangleIndices.Add(2);
mesh.TriangleIndices.Add(3);
mesh.TriangleIndices.Add(0);
mesh.Normals.Add(normal);
As far as texturing is concerned, I strongly advise you to read Daniel Lehenbauer's Blog. We need to deal with 4 points, but in 2D this time :
var q0 = new Point(0, 0);
var q1 = new Point(1, 0);
var q2 = new Point(1, 1);
var q3 = new Point(0, 1);
As you've read in Daniel's post, there is a big difference between 3D and 2D texture conventions. In 3D, you have :




But 2D texturing uses :






So the coordinates associations are :
  • p0 <-> q3
  • p1 <-> q2
  • p2 <-> q1
  • p3 <-> q0
We just need to add the texture points to the mesh for each triangle point :
mesh.TextureCoordinates.Add(q3);
mesh.TextureCoordinates.Add(q2);
mesh.TextureCoordinates.Add(q1);

mesh.TextureCoordinates.Add(q0);
mesh.TextureCoordinates.Add(q1);
mesh.TextureCoordinates.Add(q2);
The mesh will be frozen for performance reasons. In the future, we will only need to transform this mesh with rotations and translations. We will not need to move the points.
This gives us a Tesselate method that will return the mesh :
private Geometry3D Tessellate()
{
  var p0 = new Point3D(-1, -1, 0);
  ...
  var mesh = new MeshGeometry3D();
  ...
  mesh.Freeze();
  return mesh;
}
This method will be part of a Cover class. This class will be the main class to deal with covers. Here are two more methods that will help us to load the texture image :
private ImageSource LoadImageSource(string imagePath)
{
  Image thumb = Image.FromFile(imagePath);
  return new BitmapImage(new Uri(imagePath, UriKind.RelativeOrAbsolute));
}
private Material LoadImage(ImageSource imSrc)
{
  return new DiffuseMaterial(new ImageBrush(imSrc));
}
We finish the class with its constructor :
using ...
namespace Ded.Tutorial.Wpf.CoverFlow.Part1
{
  class Cover : ModelVisual3D
  {
    #region Fields
    private readonly Model3DGroup modelGroup;
    #endregion
    #region Private stuff
    private Vector3D CalculateNormal(Point3D p0, Point3D p1, Point3D p2)...
    private Geometry3D Tessellate()...
    private ImageSource LoadImageSource(string imagePath)...
    private Material LoadImage(ImageSource imSrc)...
    #endregion
    public Cover(string imagePath)
    {
      ImageSource imSrc = LoadImageSource(imagePath);
       modelGroup = new Model3DGroup();
       modelGroup.Children.Add(new GeometryModel3D(Tessellate(), LoadImage(imSrc)));
       Content = modelGroup;
    }
  }
}
Let's load this class in an empty WPF application.

We place the camera at (0, 0, 3). We add a light source so that we will not see black objects.

We also add an empty ModelVisual3D object that will contain our single cover (for now).

Here is the xaml code :
<Window x:Class="Ded.Tutorial.Wpf.CoverFlow.Part1.TestWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 Title="WPF Coverflow" Height="320" Width="512">
<Grid>
 <Viewport3D x:Name="viewPort" Grid.Column="0" Grid.Row="0" ClipToBounds="False">
   <Viewport3D.Camera>
     <PerspectiveCamera x:Name="camera" Position="0,0,3"
       UpDirection="0,1,0" LookDirection="0,0,-1"
       FieldOfView="100" NearPlaneDistance="0.125"/>
   </Viewport3D.Camera>
   <Viewport3D.Children>
     <ModelVisual3D>
       <ModelVisual3D.Content>
         <DirectionalLight Color="White" Direction="0,0,-4" />
       </ModelVisual3D.Content>
     </ModelVisual3D>
     <ModelVisual3D x:Name="visualModel">
     </ModelVisual3D>
   </Viewport3D.Children>
 </Viewport3D>
</Grid>
</Window>
This simple application will display :This can be prettier if we add some background :
<Grid.Background>
 <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
   <LinearGradientBrush.GradientStops>
     <GradientStop Color="Black" Offset="0"/>
     <GradientStop Color="#696988" Offset="1"/>
   </LinearGradientBrush.GradientStops>
 </LinearGradientBrush>
</Grid.Background>

Continue with Part 2.

Edit 2011-06-07 : Only one normal is needed per triangle. Source code has not been updated.
Edit 2014-02-23 : Code has moved to github.