Search code examples
wpf3deffectsperspective

WPF and 3D to create a bowl effect


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


Solution

  • 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;
        }
    
    }
    }