Click here to Skip to main content
15,867,756 members
Articles / Desktop Programming / WPF
Article

Using 2D controls in a 3D environment

Rate me:
Please Sign up or sign in to vote.
4.54/5 (11 votes)
26 Aug 20064 min read 122.7K   1.3K   29   23
WPF allows users to create rich 3D interfaces, but it cripples productivity by not allowing a standard way of using the 2D controls in such an interface. Let's see if it can be done manually.

Introduction

When you first heard about the 3D possibilities of WPF, you probably thought that it's a great improvement. But then you discovered that the existing 2D controls become obsolete when you make the decision of using a 3D interface.

The goal of this article is to explain the necessary steps you have to take in order to reuse the power of your existing controls in a 3D application. We will do this by example, building the 3D correspondent of a generic 2D listbox (by generic, we understand that the list elements can be any 2D control).

Sample Image - 2d-3dinteraction.png

The Code

To run the code correctly, you will need Visual Studio 2005 with the Orcas extensions from June and the .NET Framework 3.0 June CTP. I am no graphic artist, but the 3D animation effects look great and could be made to look even better.

Basic 3D Theory and Practice

The first prerequisite required to understand this article is having a basic understanding of 3D theory and how 3D interfaces are built in WPF. Dont worry if you don't have this knowledge though, this article contains all the information that is required and a bit more. 

The Problem

Let's analyse the problem first. I hope you all read the article on 3D in WPF, because we will start using that info right now.

In short, to create a 3D interface in WPF, you create a ViewPort instance. The ViewPort object will contain as children all 3D objects that you will see at runtime. These objects are are in fact sets of triangles that get defined by three points (remember the order counts!).

It is on these 3D objects (models) that we have to put the 2D controls that we want to use; the problem is: how do we do it?

The Solution

We will render the visual aspect of the control on a model just as we would put a texture on it. To do this, when we create the model, we will have to specify a mapping between the 3D coordinate system of the model and the 2D one of the control. For at least three Point3D objects on the model, we have to specify a corresponding Point2D on the control. The function that creates a cube model now becomes:

C#
private void createCubeModel()
{
    Material material;
    GeometryModel3D triangleModel;
    triangleMesh = new MeshGeometry3D();
    //Create the points we will use to define the surfaces.
    Point3D point0 = new Point3D(1, 5, 5);
    Point3D point1 = new Point3D(1, 5, -5);
    Point3D point2 = new Point3D(-1, 5, 5);
    Point3D point3 = new Point3D(-1, 5, -5);
    Point3D point4 = new Point3D(1, -5, 5);
    Point3D point5 = new Point3D(1, -5, -5);
    Point3D point6 = new Point3D(-1, -5, 5);
    Point3D point7 = new Point3D(-1, -5, -5);
    
    //Add the points and map texture coordonates to them.
    triangleMesh.Positions.Add(point0);
    triangleMesh.TextureCoordinates.Add(new System.Windows.Point(0, 2000));
    triangleMesh.Positions.Add(point1);
    triangleMesh.TextureCoordinates.Add(new System.Windows.Point(2000, 2000));
    triangleMesh.Positions.Add(point2);
    triangleMesh.TextureCoordinates.Add(new System.Windows.Point(0, 0));
    triangleMesh.Positions.Add(point3);
    triangleMesh.TextureCoordinates.Add(new System.Windows.Point(2000, 0));
    
    //Add the rest of the points, we don't need any more
    //texture mapping for them.
    triangleMesh.Positions.Add(point4);
    triangleMesh.Positions.Add(point5);
    triangleMesh.Positions.Add(point6);
    triangleMesh.Positions.Add(point7);
    
    //Start defining the surfaces that define the cube, triangle by triangle
    triangleMesh.TriangleIndices.Add(2);
    triangleMesh.TriangleIndices.Add(0);
    triangleMesh.TriangleIndices.Add(3);
    triangleMesh.TriangleIndices.Add(3);
    triangleMesh.TriangleIndices.Add(0);
    triangleMesh.TriangleIndices.Add(1);
    
    //Add the other triangles

Now we have to actually put the control on the model. We do this by using the newly introduced Brush class: the VisualBrush class. This class takes the underying Visual object of any control and creates a normal texture from it. Then, we apply this texture on our model.

C#
public void Add(Visual l)
{

    //Store the Visual of the control in the ArrayList, we'll need it later
    texts.Add(l);
    
    //Create the model on which to draw the control's Visual
    ModelVisual3D extraCube = new ModelVisual3D();
    Model3DGroup modelGroup = new Model3DGroup();
    GeometryModel3D model3d = new GeometryModel3D();
    
    //use the cube as the standard geometry of the model
    model3d.Geometry = (MeshGeometry3D)triangleMesh;
    
    //Create the material with the texture of the Control and use it on the model
    DiffuseMaterial visualMaterial = new DiffuseMaterial(new VisualBrush(l));
    model3d.Material = visualMaterial; 
    modelGroup.Children.Add(model3d);
    extraCube.Content = modelGroup;
    
    //Store the model in the ArayList, we'll need it for hittesting
    models.Add(model3d);
    
    //Add the new cube to the ViewPort, so that we can see it
    mainViewport.Children.Add(extraCube);

To remove a list item, we just delete the corresponding ArrayList item, Clear the ViewPort, recreate the models, and then put the remaining Visuals on the models.

The ViewPort Hit-Testing Mechanism

Now, we have the models with the controls rendered on them and add-remove capabilities.

But, alas! Things are not that simple! WPF does not and will not include an interactive VisualBrush in its first release. So if you test the application, you'll see that you have no interaction with the control. We must provide this functionality manually, with the help of the ViewPort hit-testing mechanism.

We will use the hit-testing offered by the ViewPort to find out which model we clicked on, or on which model is the mouse currently located. The hit-testing mechanism is based on projecting a ray from the mouse location throught the ViewPort, and if that ray hits a model, then we have results. At the end of the function, hitgeo will contain the geometry hit.

C#
void mainViewport_MouseDown(object sender, MouseButtonEventArgs e)
{
    
    System.Windows.Point mouseposition = e.GetPosition(mainViewport);
    Point3D testpoint3D = new Point3D(mouseposition.X, mouseposition.Y, 0);
    Vector3D testdirection = new Vector3D(mouseposition.X, mouseposition.Y, 10);
    
    PointHitTestParameters pointparams = 
             new PointHitTestParameters(mouseposition);
    RayHitTestParameters rayparams = 
             new RayHitTestParameters(testpoint3D, testdirection);
    
    //test for a result in the Viewport3D     
    hitgeo = null;    
    VisualTreeHelper.HitTest(mainViewport, null, HTResult, pointparams);
}
C#
private HitTestResultBehavior 
        HTResult(System.Windows.Media.HitTestResult rawresult)
{

    RayHitTestResult rayResult = rawresult as RayHitTestResult;

    if (rayResult != null)
    {

        DiffuseMaterial darkSide = 
             new DiffuseMaterial(new SolidColorBrush(
             System.Windows.Media.Colors.Red));
        bool gasit = false;
        for (int i=0;i<models.Count;i++ )
            if ((GeometryModel3D)models[i] == rayResult.ModelHit) 
            { 
                hitgeo = (GeometryModel3D)rayResult.ModelHit;
                gasit = true;
            }
        if (!gasit)  {hitgeo=null;}
        //lst.Items.RemoveAt(1);
    }
    
    return HitTestResultBehavior.Stop;
}

As you can see, we iterate through a vector of models to find the index of the model that was hit. That is because our list is composed of multiple models, each representing an element. We could have took all the elements, put them in a <CODE>StackPanel, and then add this StackPanel to a single model. Then, we wouldn't need to find out which model was hit, but rather calculate, from the point that was hit, the item that is located at that position.

Because having each element on a different model allows interesting effects, we choose this option.

Generic Listbox, You Say?

The controls are rendered on the 3D model by using their corresponding Visual, which means that we can add any object with a visual appearance on a .NET 3.0 window. Even with panels, all their contents can be rendered in a single stroke. The following code is just a demonstration of these powerfull capabilities; it doesn't look like much, but demonstrates something.

C#
private void AddComplexObject()
{

    DockPanel dp = new DockPanel();
    dp.Background = Brushes.YellowGreen;
    Label y = new Label();
    y.Background = Brushes.YellowGreen;
    y.Content = "Year";
    y.Margin = new Thickness(2);
    dp.Children.Add(y);
    DockPanel.SetDock(y, Dock.Top);
    TextBox m = new TextBox();
    m.Text = "Month";
    m.Margin = new Thickness(2);
    dp.Children.Add(m);
    DockPanel.SetDock(m, Dock.Bottom);
    Add(dp);
}

Animation Effects

Let's complete the ListBox implementation by adding some nice effects. Because the elements are positioned one on top of the other, we could create a drawer-like animation. That is, when the user puts his mouse on an element, it comes closer, like opening a drawer. programmaticaly, the animation maps to a transform called TranslateTransform; it affects the object's coordinates.

To do this, we use this function, applied to the model on which the mouse is located:

C#
void ApplyTransform(GeometryModel3D model)
{
    TranslateTransform3D tt3d = 
       new TranslateTransform3D(new Vector3D(0, 0, 0));
    DoubleAnimation da = new DoubleAnimation(-4, 
                         new Duration(TimeSpan.FromSeconds(1)));
    tt3d.BeginAnimation(TranslateTransform3D.OffsetYProperty, da);
    Transform3DGroup tgroup = new Transform3DGroup();
    tgroup.Children.Add(tt3d);
    model.Transform = tgroup;
}

Usage

If you just want to use the code, then you should remember this: put your objects in the texts ArrayList and then call the RePopulate() function to update the list. Call the function again after each change done to the ArrayList. Have fun!


License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer
Romania Romania
Dragos is currently a student at the Polytechnic University of Bucharest. He likes keeping up to date with new and amazing technologies and he hopes he will one day master the mechanisms behind modern day programming languages so that he can write the best possible code from a performance and maintainability point of view.

He keeps track of the things he learns on a daily basis on his blog, at http://picobit.wordpress.com/ .

Comments and Discussions

 
GeneralSalut Pin
Alex Manolescu5-Jan-10 10:39
Alex Manolescu5-Jan-10 10:39 
Questionserialport control with wpf form? Pin
Mir_As13-Apr-07 1:42
Mir_As13-Apr-07 1:42 
GeneralThanks for referencing my article Pin
Mike Hodnick3-Jan-07 6:09
Mike Hodnick3-Jan-07 6:09 
General2D Controls in a 3D Scene is now possible Pin
Josh Smith29-Dec-06 9:57
Josh Smith29-Dec-06 9:57 
GeneralRe: 2D Controls in a 3D Scene is now possible Pin
Dragos Sbirlea3-Jan-07 6:48
Dragos Sbirlea3-Jan-07 6:48 
Generalvery good Pin
zoujing28-Sep-06 21:35
zoujing28-Sep-06 21:35 
GeneralRe: very good Pin
Dragos Sbirlea28-Sep-06 21:37
Dragos Sbirlea28-Sep-06 21:37 
GeneralRe: A question Pin
zoujing28-Sep-06 22:04
zoujing28-Sep-06 22:04 
GeneralRe: A question Pin
Dragos Sbirlea28-Sep-06 22:11
Dragos Sbirlea28-Sep-06 22:11 
GeneralRe: A question Pin
zoujing28-Sep-06 22:41
zoujing28-Sep-06 22:41 
GeneralRe: A question Pin
Dragos Sbirlea28-Sep-06 22:45
Dragos Sbirlea28-Sep-06 22:45 
GeneralRe: A question Pin
zoujing28-Sep-06 23:07
zoujing28-Sep-06 23:07 
GeneralRe: A question Pin
zoujing28-Sep-06 22:45
zoujing28-Sep-06 22:45 
GeneralRe: A question Pin
zoujing28-Sep-06 22:54
zoujing28-Sep-06 22:54 
GeneralRe: A question Pin
Dragos Sbirlea28-Sep-06 23:07
Dragos Sbirlea28-Sep-06 23:07 
GeneralRe: A question Pin
Dragos Sbirlea28-Sep-06 23:25
Dragos Sbirlea28-Sep-06 23:25 
GeneralHow to get texture Coordinate when I click 3d model in Viewport3D ? Pin
zoujing12-Oct-06 0:13
zoujing12-Oct-06 0:13 
GeneralReally useful Pin
Richard Houltz2-Sep-06 0:51
Richard Houltz2-Sep-06 0:51 
This looks very useful. Right to the point to what I need. Thanks!!
Great article. I'll try it out for sure.
/Richard
GeneralRe: Really useful Pin
Dragos Sbirlea2-Sep-06 1:03
Dragos Sbirlea2-Sep-06 1:03 
Generalxaml Pin
dacian_herbei29-Aug-06 2:06
dacian_herbei29-Aug-06 2:06 
GeneralRe: xaml Pin
Sbarlea Claudia29-Aug-06 2:54
Sbarlea Claudia29-Aug-06 2:54 
GeneralRe: xaml Pin
dacian_herbei29-Aug-06 3:18
dacian_herbei29-Aug-06 3:18 
GeneralRe: xaml Pin
Sbarlea Claudia29-Aug-06 3:32
Sbarlea Claudia29-Aug-06 3:32 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.