views:

545

answers:

2

Hi Everyone,

How would one go about either using the 3D components of WPF or using a pseudo 3D effect to create a "Bowl" effect, where the user is looking down on a bowl and can drag around rectangles and have the rectangles perspective change so that it looks like they move up, down and around the bowl? I'm not after any gravity effects or anything, just when the items move, I need their perspective to be adjusted...

EDIT: I have been looking into the actual 3D effects available in WPF which do seem very very powerful, so maybe someone could help in getting a half sphere on my app and then binging some 3D meshes (rectangles) to its surface?

Any thoughts?

Thanks, Mark

+1  A: 

Here you go, I wasn't quite sure what you meant about the rectangles so I've just added four red rectangles around the opening of a green bowl.

Cheers,

Andy

Xaml first ...

<Window x:Class="wpfbowl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="500"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
    <Transform3DGroup x:Key="WorldTrans">

        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D x:Name="myAngleRotation" Axis="0,0,1" Angle="{Binding Rotation}" />
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
    </Transform3DGroup>
</Window.Resources>
    <StackPanel>
    <Viewport3D Name="mainViewport" ClipToBounds="True" HorizontalAlignment="Stretch" Margin="0" Height="500" >
        <Viewport3D.Camera>
            <PerspectiveCamera 
          LookDirection="0,5,0"
          UpDirection="0,0,1"
          Position="0,-10,0" 
          />
        </Viewport3D.Camera>

        <ModelVisual3D  >
        <ModelVisual3D>
            <ModelVisual3D.Content>

                <Model3DGroup>

                    <PointLight Position="0,-10,0" Range="150" Color="White" />


                    </Model3DGroup>

            </ModelVisual3D.Content>
        </ModelVisual3D>
    </ModelVisual3D>


    <ModelVisual3D Transform="{StaticResource WorldTrans}">
        <ModelVisual3D Content="{Binding Models}">

        </ModelVisual3D>
    </ModelVisual3D>

</Viewport3D>
    </StackPanel>
</Window>

... and heres the code behind ...

using System;
using System.ComponentModel;
using System.Timers;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Threading;

namespace wpfbowl
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : INotifyPropertyChanged
{
    public Window1()
    {
        InitModels();
        InitializeComponent();

        _timer = new Timer(100);
        _timer.Elapsed += TimerElapsed;
        _timer.Enabled = true;
    }

    void TimerElapsed(object sender, ElapsedEventArgs e)
    {
        Dispatcher.Invoke(DispatcherPriority.Normal, new Action<double>(Transform), 2);
    }

    private void Transform(double value)
    {
        Rotation += value;
    }

    public void InitModels()
    {
        const int bowlQuality = 20;
        Models = new Model3DGroup();
        var sphere = CreateBowl(new Point3D(0, 0, 0), 3, bowlQuality, bowlQuality, GetSurfaceMaterial(Colors.Green));

        Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(3, 0, 0), new Size3D(1.5, 0.2, 2)));
        Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(-3, 0, 0), new Size3D(1.5, 0.2, 2)));
        Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(0, 0, 3), new Size3D(1.5, 0.2, 2)));
        Models.Children.Add(GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(0, 0, -3), new Size3D(1.5, 0.2, 2)));
        Models.Children.Add(sphere);
    }


    private readonly Timer _timer;

    public Model3DGroup Models { get; set; }
    private double _rotation;
    public double Rotation
    {
        get { return _rotation; }
        set
        {
            if (_rotation == value) return;
            _rotation = value;
            OnPropertyChanged("Rotation");
        }
    }

    public static Model3DGroup CreateBowl(Point3D center, double radius, int u, int v, MaterialGroup materialGroup)
    {
        var bowl = new Model3DGroup();
        if (u < 2 || v < 2) return null;
        var pts = new Point3D[u, v];
        for (var i = 0; i < u; i++)
        {
            for (var j = 0; j < v; j++)
            {
                pts[i, j] = GetPosition(radius, i * 180 / (u - 1), j * 360 / (v - 1));
                pts[i, j] += (Vector3D)center;
            }
        }

        var p = new Point3D[4];
        for (var i = 0; i < (u /2) - 1; i++)
        {
            for (var j = 0; j <  v   - 1; j++)
            {
                p[0] = pts[i, j];
                p[1] = pts[i + 1, j];
                p[2] = pts[i + 1, j + 1];
                p[3] = pts[i, j + 1];
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[1], p[2]));
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[1], p[0]));
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[3], p[0]));
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[3], p[2]));
            }
        }
        return bowl;
    }


    private static Model3DGroup CreateTriangleModel(Material material, Point3D p0, Point3D p1, Point3D p2)
    {
        var mesh = new MeshGeometry3D();
        mesh.Positions.Add(p0);
        mesh.Positions.Add(p1);
        mesh.Positions.Add(p2);
        mesh.TriangleIndices.Add(0);
        mesh.TriangleIndices.Add(1);
        mesh.TriangleIndices.Add(2);
        var normal = CalculateNormal(p0, p1, p2);
        mesh.Normals.Add(normal);
        mesh.Normals.Add(normal);
        mesh.Normals.Add(normal);

        var model = new GeometryModel3D(mesh, material);

        var group = new Model3DGroup();
        group.Children.Add(model);
        return group;
    }

    private static 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);
    }

    private static Point3D GetPosition(double radius, double theta, double phi)
    {
        var pt = new Point3D();
        var snt = Math.Sin(theta * Math.PI / 180);
        var cnt = Math.Cos(theta * Math.PI / 180);
        var snp = Math.Sin(phi * Math.PI / 180);
        var cnp = Math.Cos(phi * Math.PI / 180);
        pt.X = radius * snt * cnp;
        pt.Y = radius * cnt;
        pt.Z = -radius * snt * snp;
        return pt;
    }

    public static MaterialGroup GetSurfaceMaterial(Color colour)
    {
        var materialGroup = new MaterialGroup();
        var emmMat = new EmissiveMaterial(new SolidColorBrush(colour));
        materialGroup.Children.Add(emmMat);
        materialGroup.Children.Add(new DiffuseMaterial(new SolidColorBrush(colour)));
        var specMat = new SpecularMaterial(new SolidColorBrush(Colors.White), 30);
        materialGroup.Children.Add(specMat);
        return materialGroup;
    }

    public static Model3DGroup GetCube(MaterialGroup materialGroup, Point3D point, Size3D size)
    {
        var farPoint = new Point3D(point.X - (size.X / 2), point.Y - (size.Y / 2), point.Z - (size.Z / 2));
        var nearPoint = new Point3D(point.X + (size.X / 2), point.Y + (size.Y / 2), point.Z + (size.Z / 2));
        var cube = new Model3DGroup();

        var p0 = new Point3D(farPoint.X, farPoint.Y, farPoint.Z);
        var p1 = new Point3D(nearPoint.X, farPoint.Y, farPoint.Z);
        var p2 = new Point3D(nearPoint.X, farPoint.Y, nearPoint.Z);
        var p3 = new Point3D(farPoint.X, farPoint.Y, nearPoint.Z);
        var p4 = new Point3D(farPoint.X, nearPoint.Y, farPoint.Z);
        var p5 = new Point3D(nearPoint.X, nearPoint.Y, farPoint.Z);
        var p6 = new Point3D(nearPoint.X, nearPoint.Y, nearPoint.Z);
        var p7 = new Point3D(farPoint.X, nearPoint.Y, nearPoint.Z);
        //front side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p3, p2, p6));
        cube.Children.Add(CreateTriangleModel(materialGroup, p3, p6, p7));
        //right side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p1, p5));
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p5, p6));
        //back side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p1, p0, p4));
        cube.Children.Add(CreateTriangleModel(materialGroup, p1, p4, p5));
        //left side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p0, p3, p7));
        cube.Children.Add(CreateTriangleModel(materialGroup, p0, p7, p4));
        //top side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p7, p6, p5));
        cube.Children.Add(CreateTriangleModel(materialGroup, p7, p5, p4));
        //bottom side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p3, p0));
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p0, p1));
        return cube;
    }


    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    #endregion

}
}
Andy Clarke
Woah! That is amazing, thanks so much Andy. I truly appreciate the detail and effort that you put in for this. What I meant by the rectangles, was that I want to be able to "drag" them around (inside) the bowl, imagine if you had a business card inside a fruit bowl and you put your finger on it then dragged it around the inside of the bowl, thats the effect I was refering to... Thanks again :)
Mark
+1  A: 

Ahh right, here you go then - you can drag the red rectangle around the bowl now - enjoy!

<Window x:Class="wpfbowl.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="500"
    DataContext="{Binding RelativeSource={RelativeSource Self}}" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseUp="Window_MouseUp" >
<Window.Resources>
    <Transform3DGroup x:Key="WorldTrans">

        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D x:Name="myAngleRotation" Axis="0,0,1" Angle="{Binding RotationLeftRight}" />

            </RotateTransform3D.Rotation>

        </RotateTransform3D>
        <RotateTransform3D>
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D x:Name="myAngleRotation2" Axis="1,0,0" Angle="{Binding RotationUpDown}" />

            </RotateTransform3D.Rotation>

        </RotateTransform3D>
    </Transform3DGroup>
</Window.Resources>
    <StackPanel>
    <Viewport3D Name="mainViewport" ClipToBounds="True" HorizontalAlignment="Stretch" Margin="0" Height="500" >
        <Viewport3D.Camera>
            <PerspectiveCamera 
          LookDirection="0,5,0"
          UpDirection="0,0,1"
          Position="0,-10,0" 
          />
        </Viewport3D.Camera>

        <ModelVisual3D  >
        <ModelVisual3D>
            <ModelVisual3D.Content>

                <Model3DGroup>

                    <PointLight Position="0,-10,0" Range="150" Color="White" />


                    </Model3DGroup>

            </ModelVisual3D.Content>
        </ModelVisual3D>
    </ModelVisual3D>


    <ModelVisual3D Transform="{StaticResource WorldTrans}">
        <ModelVisual3D Content="{Binding Models}">

        </ModelVisual3D>
    </ModelVisual3D>
        <ModelVisual3D >
            <ModelVisual3D Content="{Binding BowlModel}">

            </ModelVisual3D>
        </ModelVisual3D>

    </Viewport3D>
    </StackPanel>
</Window>

and the code behind ...

using System;
using System.ComponentModel;
using System.Timers;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
using System.Windows;
using System.Windows.Input;

namespace wpfbowl
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : INotifyPropertyChanged
{
    public Window1()
    {
        InitModels();
        InitializeComponent();

    }

    private Model3DGroup _cube;
    private bool _cubeSelected;
    private bool _cubeMoving;
    private Point3D _startPoint;
    private Point3D _currentPoint;

    public void InitModels()
    {
        const int bowlQuality = 20;
        Models = new Model3DGroup();
        BowlModel = new Model3DGroup();

        _cube = GetCube(GetSurfaceMaterial(Colors.Red), new Point3D(0, 2.6, 0), new Size3D(1.5, 0.2, 2));
        Models.Children.Add(_cube);

        var bowl = CreateBowl(new Point3D(0, 0, 0), 3, bowlQuality, bowlQuality, GetSurfaceMaterial(Colors.Green));
        BowlModel.Children.Add(bowl);
    }


    private readonly Timer _timer;

    public Model3DGroup Models { get; set; }

    public Model3DGroup BowlModel { get; set; }

    private double _rotationLeftRight;
    public double RotationLeftRight
    {
        get { return _rotationLeftRight; }
        set
        {
            if (_rotationLeftRight == value) return;
            _rotationLeftRight = value;
            OnPropertyChanged("RotationLeftRight");
        }
    }

    private double _rotationUpDown;
    public double RotationUpDown
    {
        get { return _rotationUpDown; }
        set
        {
            if (_rotationUpDown == value) return;
            _rotationUpDown = value;
            OnPropertyChanged("RotationUpDown");
        }
    }


    public static Model3DGroup CreateBowl(Point3D center, double radius, int u, int v, MaterialGroup materialGroup)
    {
        var bowl = new Model3DGroup();
        if (u < 2 || v < 2) return null;
        var pts = new Point3D[u, v];
        for (var i = 0; i < u; i++)
        {
            for (var j = 0; j < v; j++)
            {
                pts[i, j] = GetPosition(radius, i * 180 / (u - 1), j * 360 / (v - 1));
                pts[i, j] += (Vector3D)center;
            }
        }

        var p = new Point3D[4];
        for (var i = 0; i < (u /2) - 1; i++)
        {
            for (var j = 0; j <  v   - 1; j++)
            {
                p[0] = pts[i, j];
                p[1] = pts[i + 1, j];
                p[2] = pts[i + 1, j + 1];
                p[3] = pts[i, j + 1];
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[1], p[2]));
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[1], p[0]));
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[2], p[3], p[0]));
                bowl.Children.Add(CreateTriangleModel(materialGroup, p[0], p[3], p[2]));
            }
        }
        return bowl;
    }


    private static Model3DGroup CreateTriangleModel(Material material, Point3D p0, Point3D p1, Point3D p2)
    {
        var mesh = new MeshGeometry3D();
        mesh.Positions.Add(p0);
        mesh.Positions.Add(p1);
        mesh.Positions.Add(p2);
        mesh.TriangleIndices.Add(0);
        mesh.TriangleIndices.Add(1);
        mesh.TriangleIndices.Add(2);
        var normal = CalculateNormal(p0, p1, p2);
        mesh.Normals.Add(normal);
        mesh.Normals.Add(normal);
        mesh.Normals.Add(normal);

        var model = new GeometryModel3D(mesh, material);

        var group = new Model3DGroup();
        group.Children.Add(model);
        return group;
    }

    private static 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);
    }

    private static Point3D GetPosition(double radius, double theta, double phi)
    {
        var pt = new Point3D();
        var snt = Math.Sin(theta * Math.PI / 180);
        var cnt = Math.Cos(theta * Math.PI / 180);
        var snp = Math.Sin(phi * Math.PI / 180);
        var cnp = Math.Cos(phi * Math.PI / 180);
        pt.X = radius * snt * cnp;
        pt.Y = radius * cnt;
        pt.Z = -radius * snt * snp;
        return pt;
    }

    public static MaterialGroup GetSurfaceMaterial(Color colour)
    {
        var materialGroup = new MaterialGroup();
        var emmMat = new EmissiveMaterial(new SolidColorBrush(colour));
        materialGroup.Children.Add(emmMat);
        materialGroup.Children.Add(new DiffuseMaterial(new SolidColorBrush(colour)));
        var specMat = new SpecularMaterial(new SolidColorBrush(Colors.White), 30);
        materialGroup.Children.Add(specMat);
        return materialGroup;
    }

    public static Model3DGroup GetCube(MaterialGroup materialGroup, Point3D point, Size3D size)
    {
        var farPoint = new Point3D(point.X - (size.X / 2), point.Y - (size.Y / 2), point.Z - (size.Z / 2));
        var nearPoint = new Point3D(point.X + (size.X / 2), point.Y + (size.Y / 2), point.Z + (size.Z / 2));
        var cube = new Model3DGroup();

        var p0 = new Point3D(farPoint.X, farPoint.Y, farPoint.Z);
        var p1 = new Point3D(nearPoint.X, farPoint.Y, farPoint.Z);
        var p2 = new Point3D(nearPoint.X, farPoint.Y, nearPoint.Z);
        var p3 = new Point3D(farPoint.X, farPoint.Y, nearPoint.Z);
        var p4 = new Point3D(farPoint.X, nearPoint.Y, farPoint.Z);
        var p5 = new Point3D(nearPoint.X, nearPoint.Y, farPoint.Z);
        var p6 = new Point3D(nearPoint.X, nearPoint.Y, nearPoint.Z);
        var p7 = new Point3D(farPoint.X, nearPoint.Y, nearPoint.Z);
        //front side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p3, p2, p6));
        cube.Children.Add(CreateTriangleModel(materialGroup, p3, p6, p7));
        //right side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p1, p5));
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p5, p6));
        //back side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p1, p0, p4));
        cube.Children.Add(CreateTriangleModel(materialGroup, p1, p4, p5));
        //left side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p0, p3, p7));
        cube.Children.Add(CreateTriangleModel(materialGroup, p0, p7, p4));
        //top side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p7, p6, p5));
        cube.Children.Add(CreateTriangleModel(materialGroup, p7, p5, p4));
        //bottom side triangles
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p3, p0));
        cube.Children.Add(CreateTriangleModel(materialGroup, p2, p0, p1));
        return cube;
    }


    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string name)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(name));
        }
    }

    #endregion

    private void Window_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {

            var mousePos = e.GetPosition(mainViewport);
            var hitParams = new PointHitTestParameters(mousePos);
            VisualTreeHelper.HitTest(mainViewport, null, ResultCallback, hitParams);


    }

    public HitTestResultBehavior ResultCallback(HitTestResult result)
    {
        // Did we hit 3D?
        var rayResult = result as RayHitTestResult;
        if (rayResult != null)
        {
            // Did we hit a MeshGeometry3D?
            var rayMeshResult = rayResult as RayMeshGeometry3DHitTestResult;

            if (rayMeshResult != null)
            {
                if (_cubeSelected)
                {
                    _cubeMoving = true;
                    _currentPoint = rayMeshResult.PointHit;
                    RotationLeftRight = (_startPoint.X - _currentPoint.X) * 15;
                    RotationUpDown = (_currentPoint.Z -_startPoint.Z)*15;
                }
                else
                {
                    var model = rayMeshResult.ModelHit;
                    foreach (var c in _cube.Children)
                    {
                        if (c.GetType() != typeof(Model3DGroup)) continue;
                        var model3DGroup = (Model3DGroup)c;
                        foreach (var sc in model3DGroup.Children)
                        {
                            if (model != sc) continue;

                            _cubeSelected = true;
                            _startPoint = rayMeshResult.PointHit;

                        }
                    } 
                }

            }
        }
        return HitTestResultBehavior.Continue;
    }

    private void Window_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
    {
        if (!_cubeSelected) return;
        var mousePos = e.GetPosition(mainViewport);
        var hitParams = new PointHitTestParameters(mousePos);
        VisualTreeHelper.HitTest(mainViewport, null, ResultCallback, hitParams);
    }

    private void Window_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (!_cubeSelected) return;
        _cubeSelected = false;
        _cubeMoving = false;
    }

}
}
Andy Clarke
That is amazing, I cant thank you enough, seriously...
Mark
no worries, I fancied the challenge! :)
Andy Clarke