Tuesday 7 October 2008

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

We now enter the final part of coding a 3D My Pictures Helix using the Windows Presentation Foundation, we’ve done quite a lot and covered quite a few key areas but still have some important areas to cover: optimising memory usage, camera positioning and hit testing. Soon you will have your very own program to impress your friends.

So last time we finally got round to adding some 3D functionality into the application in the form of an actual spiral and I gave a flavour of what was to come on my blog http://blogplusequals.blogspot.com/. However there is a still lot to do, starting with changing the way images are loaded into the application for the image brush. Currently we load in the entire image for the image brush, which is hugely expensive in terms of memory requirements if the images are that taken even by a regular camera. Another big feature we need to add is the ability to navigate the helix in order to view all the pictures on it.

So why is the current method of content loading so bad – creating an imagebrush directly from an image? It loads the entire image into memory, as if we were to use it directly without scaling, which is very inefficient as we are using a reduced size thumbnail in the helix. Therefore we should instead preload the image and create a thumbnail from it that we can use instead; thus saving us considerable amounts of memory while also speeding up the rendering times. Let’s get started then! Go to the CreateHelix() function and replace the texture loading code as follows:



Now the fancy stuff actually takes place in the function CreateThumbnail, in that function it loads the original image, resizes it and returns the new smaller image with a overall reduced memory requirement.



When the application loads it now creates thumbnails however we still have a problem. The program is constantly busy loading the images and may not respond to system events, now in a normal C# application you could use System.DoEvents() however that’s missing in WPF so we can create our own substitute to help combat this issue and to allow the application to remain responsive.



Add the above using statements so we can use the Threading functionality, and then anywhere in the class add the following function to act like the missing DoEvents()... Now please don’t complain at me, I’d rather not want to get lots of emails about how it doesn’t exactly work like DoEvents() or how really we shouldn’t even need it in WPF, I’m just adding in the functionality to provide a simple, easy to understand fix to the problem.



The first place we can add this is to the image loading loop, partly to make it more responsive to system input and also it then creates the effect of the helix filling up as the images are loaded. Just before the Catch statement near the end of CreateHelix you can add a call to DoEvents()



So now we have all the code needed to create the helix, but the images are quite small so we want to display them larger using the code we implemented in the first article, so now we have to add code to make the surfaces respond.
Firstly in order to do this we create a dictionary to bind the surfaces geometry to the filename of the picture it is showing, in the classes declarations add the following.



Adding an entry to the dictionary is very simple and is done in the CreateTexturedTriangleModel function (remember we had one of the arguments being the pictures filename), we need to place the code near the end of the function right after we declare the GeometryModel3D.



Now whenever we perform a lookup in the dictionary for the GeometryModel3D we will be returned with the filename of the picture it was showing.
Inorder to work out when you click which image you were actually clicking we have to write the code to perform a hit test on the GeometryModel, therefore we need to start off by adding an event handler for the Window_MouseLeftButtonUp event.



To perform a hit test we have to get the mouse position and then using the VisualTreeHelper we construct a HitTest function to go through the surfaces and see if any have been hit by the mouse, if so return their GeometryModel (from which we get the pictures filename using the dictionary).



We start with in the code above getting the position of the mouse and creating a 3D point version of it using 0 as the Z value, we then move through to 10 as the Z value and perform the HitTest in between. The HitTest itself is performed on mainViewport and the resultCallBack function of VisualTreeHelper.HitTest we use is called HTResult, which we shall program shortly.
The hit test callback value is a function that is called if there is a result, if it has hit something we then check if its hit a surface(GeometryModel3D) and then we can do something with the result(Display the image)



So when we have called a hit test and it is successful we now need to write the code to display a large image of a picture. We can use the DisplayImage function we made in the first article for this, replace the Do actions... comment in the previous piece of code with the following to display a full window preview of the image that is being clicked on.



This handles clicking on a surface and displaying a large preview but doesn’t handle when we then click on the preview afterwards to close it. For this we have to add some more code to the start of Window_MouseLeftButtonUp which will detect if the preview window is visible and if so fade it out for 500 milliseconds.



At this point we now have the helix and large image previews when we click on a surface, all we have to do now is add the rotation functionality. The rotation will be controlled by three buttons on the form; Rotate Left, Stop and Rotate Right, I will be using the standard button controls for this but you could use something more fancy; such as using Expression Blend to create movie style controls. A great source of inspiration would be the button gallery example project in Expression Blend 2, however explaining how to make WPF controls would take an entire article so go explore!
Back to the xaml designer now where we add each of the three buttons.



The buttons are centred at the bottom of the window and each have a glow effect to make them slightly more interesting. Each of the buttons have their own click events which we will handle in the C# Code. In the code to make it more readable we will use an enumeration to abstract the directions so the code will be easier to read.



Insert the above code in the class declarations, setting by default the rotation to be stationary so the helix won’t automatically start when we load the application.
The rotation could be done by multiple different means, now usually this could be done by an animation but I wanted to be slightly different, I will use a render loop (bit like what you would see in a game) to move the camera around so I can talk a bit more about threading in WPF. To start off with we need to create the Render loop and call it once everything has loaded in the classes constructor. For this we can create a new function called MainLoop() and call it from the constructor.



We now need to start programming the Main Loop. We need it to store the start time of the frame so we can work out the virtual time the frame took to render (so that the rotation remains smooth) and also we use an external Boolean that we tie into the Window_Closing() event. This is so that the render loop stops when we close the form, otherwise it may continue the thread even after we close the form, which isn’t good.



To move the camera we are going to create a helper function that will when given an angle, it will move the camera around the helix to that point. We’ll need a variable in the MainLoop to store the current angle of the camera but also we will have to add a class variable to store the maximum angle that the helix produced (so we don’t rotate any further).



We’ll use a new class variable variable MaxCameraAngle to store the maximum rotation, this will get assigned to at the very end of CreateHelix before it returns, the value that is assigned to it will be value of RotationAngle; a locally defined double variable that stored the Angle of the pictures as they were added.



Finally we can now go back to Mainloop and add the camera controlling code. Firstly define a local double CameraAngle at the top of the function which will keep track of where the camera is and then add the following code to the main loop where comment ‘Rotation update code goes here’ resides.
Now for just one final thing, before we call CreateHelix in the class constructor add a call to MoveCamera with the angle 0. This will reset it ready for us to use.



Now there we have it, a fully functional application. Run it and see for yourself!



Now there are lots of things you can do to add to this, replacing the buttons being one thing, possibly automating the rotation and creating a screensaver could be another. Well I hope you have enjoyed this coding fest, tune in next time for some more interesting coding projects or visit my blog at http://blogplusequals.blogspot.com/

5 comments:

Anonymous said...

Hi...Sry to post again but I couldnt find the Rotation commands in the Main Loop in the post...Could u pls check that out?

Daevil said...

Hi Kevin,
impressive post! I'm an SP @ Microsoft Italy, I wish one day to be able to post cool stuff like you do ;). As kay said, I think you missed some code to the MainLoop routine.
Here it goes :)

int LastFrame = Environment.TickCount;
double CameraAngle = 0.0;

while (Running)
{
//work out the time it took to render last frame
int FrameTime = Environment.TickCount - LastFrame;
LastFrame = Environment.TickCount;

//Rotation Update Code
if (RotationDirection == RotationDirections.Clockwise
&& CameraAngle <= MaxCameraAngle)
{
CameraAngle += 0.02;
MoveCamera(CameraAngle);
}
else if (RotationDirection == RotationDirections.AntiClockwise
&& CameraAngle >= 0)
{
CameraAngle -= 0.02;
MoveCamera(CameraAngle);
}
//...the other code

keep up posting!
Dave

3aSef said...

Hi Kevin

Thanks for your an amazing Post

I tried to make your application
and there is no errors
but the problem that nothing appeared !!

Do you know what was the problem !?


Thanks

Asef

Jon said...

i have the same problem with nothing happening unless i comment out the main loop call.

also, my images look like they're stepped? i'm wondering why it looks like this. :\

screenshot here

Jon said...

never mind -- i fiddled with it a bit and got it to work a bit better by putting the main loop into a backgroundworker.

also, fixed the stepped look of the images by increasing the number of spiralsegments:

int spiralSegments = (int)(SPIRALSEGMENTSPERROTATION / (2 * Math.PI) * angleCovered) * 20;