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.

12 comments:

Leonardo said...

Hi Ded, i'm brazilian, sorry for my english.

I have one question :

How i rotate the cover 180ยบ (degress) ?

I try, but the texture doesn't appear, i need to invert "normals" ? How i do this ?

U can explain for me?

And , i want to create a box, like a dvd case, with 6 faces, and aplly texture in all faces, one texture to each face.

U can help me?

Tkx.

Leonardo Mendes

ded said...

Hi Leonardo,

Transformations are detailled in part 2 of this tutorial. To rotate a cover, you need the angle (180° here) but you also have to define which around axis the rotation will occur.

If the texture does not appear, this can be many things (like the camera).

For a box, you need six objects if the texture is specific to each face...

VaruN said...
This comment has been removed by the author.
VaruN said...

The mapping is defined by:
# p0 <-> q3
# p1 <-> q2
# p2 <-> q1
# p3 <-> q0

Whereas for the second triangle the points added are p2, p3, p0. Therefore the corresponding TextureCoordinates should have been q1, q0, q3. But in the code the mapping is added as q0, q1, q2. Can you please clarify?
Thanks,
-Varun.

paul said...

Hi

I LOVE the coverflow. Could you please let me know how I could use a (databound) custom datatemplate (i.e. like a listbox) with the control? So instead of displaying the images, I could display custom data within the control.

Many thanks and excellent work :)

Nase said...

Thanks Ded for your example which saved me a lot of time. Thanks also for your good images instead of trying it to explain by text.

Kok Jin said...

Hi Ded, just found out your site recently and it is a good tutorial.

I have a question, is it possible to load the cover flow with videos instead of images?

ded said...

Hi Kok Jin ! It should be possible to include videos, but this is not detailed in the tutorial. Actually, it would be great to improve this tutorial to use ContentTemplates to ease customization...

Kok Jin said...

If I am about to include videos into my cover flow. It is sufficient if I change the context inside the class ImageSource, LoadImage and LoadImageMirror?

plus, do you have any demo on how to use ContentTemplates to ease customization??

BalaG M said...

Hi ded,
Thanks for this really good tutorial. I am facing one issue here...
I have some mouseover event(style change) for the image control that is added to the modelGroup.Children
For some reason the events are not firing. Will the modelgroup block the binding events of its child?

ded said...

@Balag_M I think this is because there is already a MouseDown handler on the viewPort.

BalaG M said...

But even the tool tip is not firing. If you can confirm whether the tool tip of the image control (modelgroup.children) works in your version, then atleast i can be sure that the problem lies in my code :)
Thanks for your time!