Wednesday 17 September 2008

My Pictures 3D Helix using WPF (Part 2 of 3)

My Pictures 3D Helix using WPF (Part 2 of 3)
Writing computer code is a funny thing really. You can always tell if you have just written bad code, especially so here at Microsoft. For if an architect sees it, normally they will pull a face as if you have just slapped them around the face with your laptop, however you also notice when you write good code for how elegant a solution it seems, so read the article below and judge for yourself.


In the last article, we started laying the groundwork for our My Pictures 3D helix by storing a list of all the pictures, writing some xaml and displaying an image with a fade in effect as well as a border. This time we are moving on to discovering the 3D power of WPF, and how we can utilise it within our project.




If you have never done 3D coding before or not used to the maths behind it then some of the topics covered may seem a bit advanced, so a few blog posts you could look into would be Daniel Lehenbauer discussing 3D texturing (http://blogs.msdn.com/danlehen/archive/2005/11/06/489627.aspx) and Mike Hodnick giving another example of how to create 3D shapes in WPF (http://www.kindohm.com/technical/WPF3DTutorial.htm)



So what do we need to do? Well we need to load the images as a brush, create curved images in the shape of a spiral and have a camera pointing at it as well as a light so we can see the 3D structure... easy!
To start with, we can create the camera element in xaml, which we then alter in the C# code at runtime, so load up the project and add the following code to the Grid control before the border control, if you do it after the border control, it will make some weird effects later!




What the code above does is creates a 3D Viewport on which we can add all our 3D objects, having the ClipToBounds set to true means it will automatically resize to fill the screen. Inside the viewport there is a PerspectiveCamera with some default settings to handle how the camera itself is displayed including a default position and look direction. To light the model we have added an ambient white light
We now want to set a few Constants to control the look and feel of the helix.




Place the above code at the top of the class along with the few other variables we declared in the last article to hold file extensions and the pictures file names.
Now lets start writing the quite epic code to create the spiral, first off we need to create a new function, I called mine CreateHelix.




Inside this function we are going to go through all the filenames we extracted and see if we can load them while spiralling round, so we need to add a variable to store the current rotation around the helix as well as adding the loop. We use a try, catch statement so the program will not crash if it tries to use an image that is corrupt or invalid.




We aren’t actually going to put anything in the catch statement as we aren’t dependent on the try being successful but it is required anyway.
So how are we going to display the images anyway? Well it’s actually annoyingly complicated for what we want to do; but in the end it creates we want. Firstly, we have to create an image brush and load it with one of the pictures we are loading from My Pictures, then create the curved surface using many segments made from triangles and then paint the surface with the image brush.
So let us start and construct the Image brush that we will be using.




For the time being we load the entire image into the image brush, which already can cause some problems as the image size of the pictures; especially if they are taken by a digital camera, are a lot larger than we need, yet for the time being we shall use this method out of simplicity. Once the image brush is created we alter some of its rendering options to increase the performance, this mainly focuses around caching the images and using a faster scaling method when rendering the image to the screen. Finally we have to set the ViewportUnits to absolute otherwise when we come to paint the triangles it won’t work.
Now we calculate a few lookup values focusing on how it fits around the helix; such as its relative width and how many segments it covers.




Now let the 3D begin! We’ll now after that piece of code have to write the code to group all the triangles together into a 3D model, and then add it to a visual model that we finally add to the viewport, so we have to create the model, add the triangles to it and then render it. Here is the outline of the code to do this. Once this is done, we need to progress the rotation counter (RotationAngle) with how much the picture covered as well as the divider.




Hopefully that seems quite straight forward though what goes inside the for loop is the more complicated part.
Diving to the triangle creation logic we again do a bit of pre-computing some values to make life easier for us. Afterwards we declare 4 points which are the corners of the segment; each segment itself being made out of 2 triangles. The segments take into account the rotation and climb of the helix so there’s quite a lot going into making their values. To use the Point3D type we have to import the Media3D namespace by adding it to the imports at the top of the file.




So now in the triangle creation logic (inside the for loop) place the following code.




Now we know the corner points of the segment we can now make the triangles inside it though we actually have to make 4 triangles rather than two due to the way that WPF renders triangles. As WPF only renders triangles that have their normal visible to the viewport, when the surface rotates around it’s no longer visible as nothing is rendered on its reverse. To solve this we have another set of triangles with the reverse normal to represent the back of the pictures so the pictures themselves are always visible as they rotate around the helix.
Its now time to write some helper functions in order to be able to create the triangle meshes, firstly we will need one to calculate the normal of the triangle points, and then another to actually create the mesh and return it to the triangle creation loop that we just worked out the points for.
To create the normal for the triangle we can use the following function.




Don’t worry about the code, regrettably I haven’t got enough space to write a maths tutorial so trust me it just works. Now that’s in place here is the code for the function we are going to use to make the triangle.




I’ve added to the functions arguments a string to pass the filename of the image that the triangle will be displayed. This is so later on we can use it to work out which picture is which when we handle a click event on the window. Again hopefully the code is easy enough to understand but trust me it works.
Now back again to the triangle creation loop after we work out the PicturePoints and now we can add the code to call the CreateTexturedTriangleModel function so we can finally create the triangles.




So all the code to create the spiral is complete but all we have to do is call it! Go to the constructor and remove the reference to DisplayImage and replace it with:




I wouldn’t suggest you running the application unless you only have a few pictures as otherwise the memory requirements will go through the roof. Tune in next time for when I show you how to counteract this by using thumbnails and also getting the helix to rotate and finally put back in the large image viewing functionality we made in the first article. If you do run the program however you should end up with something like this.



13 comments:

Anonymous said...

ok. I found an information here that i want to look for.

Anonymous said...

its good to know about it? where did you get that information?

Anonymous said...

very cool.

Anonymous said...

wow, very special, i like it.

Anonymous said...

very cool.

Anonymous said...

what happened to the other one?

Nitima Sood said...

Quite entertaining

Check out my blog at http://offbeatspirituality.blogspot.com/

Thanks

Adnan said...
This comment has been removed by a blog administrator.
Anonymous said...

very awsome.

Anonymous said...

thx for the reference. Happy 3D coding!

-Mike

holsee said...

Great Work dude! Cool idea :)!
Keep up the good (WPF) work!

Ping Back - Http://holsee.blogspot.com

Anonymous said...

Hi.. dont seem to get the shape of the helix...and i follewed the steps but nt able to get the skewed rectangle of each picture...it like a triangle and the brush resource is nt proper...

here is the code...

private void CreateHelix()
{
double rotationanglee = 0.0;
foreach (string filename in FList)
{
try
{
ImageBrush _pictbrush = new ImageBrush(new BitmapImage(new Uri(filename)));
RenderOptions.SetCachingHint(_pictbrush, CachingHint.Cache);
RenderOptions.SetBitmapScalingMode(_pictbrush, BitmapScalingMode.LowQuality);
_pictbrush.ViewboxUnits = BrushMappingMode.Absolute;

double widthsize = spiral_Height / _pictbrush.ImageSource.Height * _pictbrush.ImageSource.Width;
double anglecovered = 1.0 / spiral_rad * widthsize;
int spiralsegments = (int)(spiral_SegmentRot / (2 * Math.PI) * anglecovered);

Model3DGroup picturemesh = new Model3DGroup();
for (int segmentindex = 0; segmentindex < spiralsegments; segmentindex++)
{
Point3D[] picturedPoints = new Point3D[4];
double Segstartangle = rotationanglee + anglecovered / spiralsegments * (segmentindex);
double SegEndangle = rotationanglee + anglecovered / spiralsegments * (segmentindex + 1);

picturedPoints[0] = new Point3D(spiral_rad * Math.Sin(SegEndangle), SegEndangle / (2 * Math.PI) * spiral_climb
+ (double)spiral_Height / 2.0, spiral_rad * Math.Cos(SegEndangle));

picturedPoints[1] = new Point3D(spiral_rad * Math.Sin(Segstartangle), Segstartangle / (2 * Math.PI) * spiral_climb
+ (double)spiral_Height / 2.0, spiral_rad * Math.Cos(Segstartangle));

picturedPoints[0] = new Point3D(spiral_rad * Math.Sin(SegEndangle), SegEndangle / (2 * Math.PI) * spiral_climb
- (double)spiral_Height / 2.0, spiral_rad * Math.Cos(SegEndangle));

picturedPoints[0] = new Point3D(spiral_rad * Math.Sin(Segstartangle), Segstartangle / (2 * Math.PI) * spiral_climb
- (double)spiral_Height / 2.0, spiral_rad * Math.Cos(Segstartangle));

picturemesh.Children.Add(CreateTriangle(filename, _pictbrush, picturedPoints[0], picturedPoints[1], picturedPoints[2],
new Point(1.0 / (double)spiralsegments * (double)(segmentindex + 1), 0.0), new Point(1.0 / (double)spiralsegments * (double)(segmentindex), 0.0),
new Point(1.0 / (double)spiralsegments * (double)(segmentindex + 1), 1.0)));


picturemesh.Children.Add(CreateTriangle(filename, _pictbrush, picturedPoints[2], picturedPoints[1], picturedPoints[3],
new Point(1.0 / (double)spiralsegments * (double)(segmentindex + 1), 1.0), new Point(1.0 / (double)spiralsegments * (double)(segmentindex), 0.0),
new Point(1.0 / (double)spiralsegments * (double)(segmentindex), 1.0)));

picturemesh.Children.Add(CreateTriangle(filename, _pictbrush, picturedPoints[1], picturedPoints[0], picturedPoints[2],
new Point(1.0 / (double)spiralsegments * (double)(segmentindex), 0.0), new Point(1.0 / (double)spiralsegments * (double)(segmentindex+1), 0.0),
new Point(1.0 / (double)spiralsegments * (double)(segmentindex + 1), 1.0)));

picturemesh.Children.Add(CreateTriangle(filename, _pictbrush, picturedPoints[3], picturedPoints[1], picturedPoints[2],
new Point(1.0 / (double)spiralsegments * (double)(segmentindex ), 1.0), new Point(1.0 / (double)spiralsegments * (double)(segmentindex), 0.0),
new Point(1.0 / (double)spiralsegments * (double)(segmentindex + 1), 1.0)));
}

ModelVisual3D model = new ModelVisual3D();
model.Content = picturemesh;
mainViewPort.Children.Add(model);
rotationanglee += anglecovered + spiral_Divider;
}
catch
{

}
}
}

Anonymous said...

Hi am sry for posting the code...bt was able to correct my mistake...thanks for the article...
pls do take out my previous comment...