WPF:3D地球仪

环境: VS2022+.Net Framework4.8+HelixToolkit3.12

一、HelixToolkit包安装

  • 通过NuGet包管理器搜索并安装 HelixToolkit 相关的包。

二、将控件添加到工具箱

  • 在项目引用好库之后,就可以把它添加到当前项目的工具箱里了。
    1. 打开工具箱
      在 Visual Studio 菜单栏选择“视图” -> “工具箱”,或使用快捷键 `Ctrl+Alt+X。
    2. 添加新选项卡(可选)
      在工具箱空白处右键,选择“添加选项卡”,给这个分组起个名字,比如 Helix Toolkit,方便管理。
    3. 选择项
      选中你新建的Helix Toolkit选项卡,右键选择“选择项...”。
    4. 浏览添加
      在弹出的“选择工具箱项”对话框中,点击“浏览...”按钮然后找到你的项目文件夹,我的是
      C:\Users\Lenovo\.nuget\packages\helixtoolkit.wpf\3.1.2\lib\net48 目录下,选择 HelixToolkit.Wpf.dll 文件并打开。
    5. 确认添加
      在“WPF 组件”列表里,你会看到新出现的 HelixViewport3D 等控件,确保它们被勾选后,点击“确定。

完成这些步骤后,工具箱里就会出现 HelixViewport3D 等控件了。之后你就可以像拖拽普通按钮一样,把它们拖到 XAML 设计视图里使用。核心用法是用 HelixViewport3DTrackballDecorator 包裹住你的 3D 场景,让鼠标交互变得非常简单

三、3D地球仪

  • 球体通过细分经纬度网格构建:

    • 顶点生成:
      遍历纬度 φ (0 到 π) 和经度 θ (0 到 2π)
      每个顶点坐标:(rcos(φ)cos(θ), rsin(φ), -rcos(φ)*sin(θ))
    • 三角形面片:
      每四个相邻顶点构成两个三角形
      形成完整的球体网格
  • 相机位置使用球面坐标系 (r, θ, φ) 控制:
    r (CameraR) = 相机到原点的距离(半径)
    θ (CameraTheta) = 水平角度(绕Y轴旋转)
    φ (CameraPhi) = 垂直角度(与水平面的夹角)

  • 纹理映射
    使用 ImageBrush 加载地球纹理图片,通过 TextureCoordinates 将纹理映射到球体表面。

四、MainWindow.xaml

<Window x:Class="WpfA_3DGlobe.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfA_3DGlobe"
        mc:Ignorable="d"
        Title="3D地球仪" Height="450" Width="800" 
        WindowStartupLocation="CenterScreen" 
        Loaded="MainWindow_Loaded" KeyDown="MainWindow_KeyDown">
      
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="250"/>
        </Grid.ColumnDefinitions>


        <!-- 右侧控制面板 -->
        <StackPanel Grid.Column="1" Margin="10">
            <TextBlock Text="相机位置:" Margin="0,0,0,5"/>
            <!-- 显示相机坐标 -->
            <Border BorderBrush="Gray" BorderThickness="1" CornerRadius="5" Padding="5" Margin="0,0,0,10">
                <StackPanel>
                    <TextBlock Name="CameraXText" Text="X: 0.00" Margin="5,2"/>
                    <TextBlock Name="CameraYText" Text="Y: 0.00" Margin="5,2"/>
                    <TextBlock Name="CameraZText" Text="Z: 0.00" Margin="5,2"/>
                    <TextBlock Name="CameraDistanceText" Text="距离: 0.00" Margin="5,2" Foreground="Green" FontWeight="Bold"/>
                    <TextBlock Name="CameraAngleText" Text="角度: 0°" Margin="5,2" Foreground="Blue"/>
                </StackPanel>
            </Border>

            <TextBlock Text="操作说明:" FontWeight="Bold" Margin="0,10,0,5"/>
            <TextBlock Text="• 鼠标拖拽旋转地球" Margin="5,2" FontSize="11"/>
            <TextBlock Text="• 键盘方向键控制视角" Margin="5,2" FontSize="11"/>
            <TextBlock Text="• +/- 键缩放" Margin="5,2" FontSize="11"/>

            <Separator Margin="0,10,0,10"/>

            <TextBlock Text="控制器:" FontWeight="Bold" Margin="0,0,0,5"/>

            <StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Left">
                <Button Name="buttonUp" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Click="buttonUp_Click">
                    <Label Content="↑" Foreground="#0f0fff" FontSize="12" Padding="5,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
                </Button>

                <StackPanel Orientation="Horizontal" Margin="5,1,5,5">
                    <Button Name="buttonLeft" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Click="buttonLeft_Click">
                        <Label Content="←" Foreground="#0f0fff" FontSize="12" Padding="2,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
                    </Button>
                    <Button Name="buttonDown" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Margin="1,0" Click="buttonDown_Click">
                        <Label Content="↓" Foreground="#0f0fff" FontSize="12" Padding="5,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
                    </Button>
                    <Button Name="buttonRight" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Click="buttonRight_Click">
                        <Label Content="→" Foreground="#0f0fff" FontSize="12" Padding="2,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
                    </Button>
                </StackPanel>
            </StackPanel>

            <!-- 重置按钮 -->
            <Button Content="重置视角" Click="ResetCamera_Click" Margin="0,10,0,0" Width="80" HorizontalAlignment="Left"/>
        </StackPanel>
        <!-- 左侧面板 -->
        <Grid Background="#000000" MouseMove="MainViewport3D_MouseMove">
            <Viewport3D Name="MainViewport3D" MouseDown="MainViewport3D_MouseDown" MouseMove="MainViewport3D_MouseMove" MouseUp="MainViewport3D_MouseUp" />

        </Grid>
    </Grid>
</Window>

五、MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfA_3DGlobe
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        // 三维 模型.
        private Model3DGroup MainModel3Dgroup = new Model3DGroup();
        // 照相机.
        private PerspectiveCamera TheCamera;

        // 照相机默认角度.
        private double CameraPhi = Math.PI / 6.0;       // 30 degrees (度)
        private double CameraTheta = Math.PI / 6.0;     // 30 degrees (度)
        private double CameraR = 3.0;

        // 点击上、下方向键,来改变CameraPhi
        private const double CameraDPhi = 0.1;
        // 点击左、右方向键,来改变CameraTheta.
        private const double CameraDTheta = 0.1;
        // 点击+、-键,来改变CameraR.
        private const double CameraDR = 0.1;

        // mouseDown标识
        private bool IsMouseDown;
        // 上一个mouseMove的点
        private Point MouseMovePoint;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            // 初始化照相机的视野位置
            TheCamera = new PerspectiveCamera();
            //照相机的水平的视野,以度为单位。 默认值为 45。
            TheCamera.FieldOfView = 60;
            MainViewport3D.Camera = TheCamera;
            PositionCamera();

            // 初始化光源。
            DefineLights();

            // 初始化地球仪对象。
            DefineModel();

            // ModelVisual3D 提供了 Visual3D 呈现的 Model3D 对象
            ModelVisual3D model_visual = new ModelVisual3D();
            // 将MainModel3Dgroup 添加到 ModelVisual3D
            model_visual.Content = MainModel3Dgroup;

            // 添加到 Viewport3D 视窗显示。
            MainViewport3D.Children.Add(model_visual);
        }

        private void MainWindow_KeyDown(object sender, KeyEventArgs e)
        {
            switch (e.Key)
            {
                case Key.Up:
                    CameraPhi += CameraDPhi;
                    if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;
                    break;
                case Key.Down:
                    CameraPhi -= CameraDPhi;
                    if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;
                    break;
                case Key.Left:
                    CameraTheta += CameraDTheta;
                    break;
                case Key.Right:
                    CameraTheta -= CameraDTheta;
                    break;
                case Key.Add:
                case Key.OemPlus:
                    CameraR -= CameraDR;
                    if (CameraR < CameraDR) CameraR = CameraDR;
                    break;
                case Key.Subtract:
                case Key.OemMinus:
                    CameraR += CameraDR;
                    break;
            }

            // 更新照相机位置.
            PositionCamera();
        }

        #region main method
        /// <summary>
        /// 初始化光源
        /// </summary>
        private void DefineLights()
        {
            //光适用 light 于对象统一,而不考虑其形状的对象。
            AmbientLight ambient_light = new AmbientLight(Colors.Gray);
            //沿 Vector3D 指定的方向投射其效果的光对象。
            DirectionalLight directional_light = new DirectionalLight(Colors.Gray, new Vector3D(-1.0, -3.0, -2.0));

            MainModel3Dgroup.Children.Add(ambient_light);
            MainModel3Dgroup.Children.Add(directional_light);
        }

        /// <summary>
        /// 初始化3D对象
        /// </summary>
        private void DefineModel()
        {
            Model3DGroup globe_model = new Model3DGroup();
            MainModel3Dgroup.Children.Add(globe_model);

            ImageBrush globe_brush = new ImageBrush(new BitmapImage(new Uri("pack://application:,,,/Resources/globe.png")));
            Material globe_material = new DiffuseMaterial(globe_brush);
            MeshGeometry3D globe_mesh = null;
            MakeSphere(globe_model, ref globe_mesh, globe_material, 1, 0, 0, 0, 20, 30);
        }

        /// <summary>
        /// 创建一个球体
        /// </summary>
        /// <param name="model_group"></param>
        /// <param name="sphere_mesh"></param>
        /// <param name="sphere_material"></param>
        /// <param name="radius"></param>
        /// <param name="cx"></param>
        /// <param name="cy"></param>
        /// <param name="cz"></param>
        /// <param name="num_phi"></param>
        /// <param name="num_theta"></param>
        private void MakeSphere(Model3DGroup model_group, ref MeshGeometry3D sphere_mesh, Material sphere_material,
            double radius, double cx, double cy, double cz, int num_phi, int num_theta)
        {
            // Make the mesh if we must.
            if (sphere_mesh == null)
            {
                sphere_mesh = new MeshGeometry3D();
                GeometryModel3D new_model = new GeometryModel3D(sphere_mesh, sphere_material);
                model_group.Children.Add(new_model);
            }

            double dphi = Math.PI / num_phi;
            double dtheta = 2 * Math.PI / num_theta;

            // Remember the first point.
            int pt0 = sphere_mesh.Positions.Count;

            // Make the points.
            double phi1 = Math.PI / 2;
            for (int p = 0; p <= num_phi; p++)
            {
                double r1 = radius * Math.Cos(phi1);
                double y1 = radius * Math.Sin(phi1);

                double theta = 0;
                for (int t = 0; t <= num_theta; t++)
                {
                    sphere_mesh.Positions.Add(new Point3D(
                        cx + r1 * Math.Cos(theta), cy + y1, cz + -r1 * Math.Sin(theta)));
                    sphere_mesh.TextureCoordinates.Add(new Point(
                        (double)t / num_theta, (double)p / num_phi));
                    theta += dtheta;
                }
                phi1 -= dphi;
            }

            // Make the triangles.
            int i1, i2, i3, i4;
            for (int p = 0; p <= num_phi - 1; p++)
            {
                i1 = p * (num_theta + 1);
                i2 = i1 + (num_theta + 1);
                for (int t = 0; t <= num_theta - 1; t++)
                {
                    i3 = i1 + 1;
                    i4 = i2 + 1;
                    sphere_mesh.TriangleIndices.Add(pt0 + i1);
                    sphere_mesh.TriangleIndices.Add(pt0 + i2);
                    sphere_mesh.TriangleIndices.Add(pt0 + i4);

                    sphere_mesh.TriangleIndices.Add(pt0 + i1);
                    sphere_mesh.TriangleIndices.Add(pt0 + i4);
                    sphere_mesh.TriangleIndices.Add(pt0 + i3);
                    i1 += 1;
                    i2 += 1;
                }
            }
        }

        /// <summary>
        /// 照相机位置
        /// </summary>
        private void PositionCamera()
        {
            // 笛卡尔坐标 计算照相机位置
            double y = CameraR * Math.Sin(CameraPhi);
            double hyp = CameraR * Math.Cos(CameraPhi);
            double x = hyp * Math.Cos(CameraTheta);
            double z = hyp * Math.Sin(CameraTheta);
            TheCamera.Position = new Point3D(x, y, z);

            // 获取或设置 Vector3D 定义在世界坐标查找照相机方向
            TheCamera.LookDirection = new Vector3D(-x, -y, -z);

            // 获取或设置 Vector3D 定义照相机的向上方向
            TheCamera.UpDirection = new Vector3D(0, 1, 0);

            // 更新显示相机坐标
            UpdateCameraDisplay(x, y, z);
        }

        /// <summary>
        /// 更新相机坐标显示
        /// </summary>
        private void UpdateCameraDisplay(double x, double y, double z)
        {
            // 使用 String.Format 替代字符串插值
            CameraXText.Text = string.Format("X: {0:F2}", x);
            CameraYText.Text = string.Format("Y: {0:F2}", y);
            CameraZText.Text = string.Format("Z: {0:F2}", z);

            double distance = Math.Sqrt(x * x + y * y + z * z);
            CameraDistanceText.Text = string.Format("距离: {0:F2}", distance);

            double angleDeg = CameraTheta * 180 / Math.PI;
            double phiDeg = CameraPhi * 180 / Math.PI;
            CameraAngleText.Text = string.Format("水平角: {0:F1}° 俯仰角: {1:F1}°", angleDeg, phiDeg);
        }

        /// <summary>
        /// 重置视角
        /// </summary>
        private void ResetCamera_Click(object sender, RoutedEventArgs e)
        {
            CameraPhi = Math.PI / 6.0;
            CameraTheta = Math.PI / 6.0;
            CameraR = 3.0;
            PositionCamera();
        }

        #endregion

        #region 鼠标拖动
        private void MainViewport3D_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                MouseMovePoint = e.GetPosition(MainViewport3D);
                IsMouseDown = true;
            }
        }

        private void MainViewport3D_MouseMove(object sender, MouseEventArgs e)
        {
            if (IsMouseDown && e.LeftButton == MouseButtonState.Pressed)
            {
                Point point = e.GetPosition(MainViewport3D);

                double x = point.X - MouseMovePoint.X;
                double y = point.Y - MouseMovePoint.Y;

                if (Math.Abs(x) > Math.Abs(y))
                {
                    CameraTheta += (x / MainViewport3D.ActualWidth * 3);
                }
                else
                {
                    CameraPhi += (y / MainViewport3D.ActualHeight * 3);
                    if (y > 0)
                    {
                        if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;
                    }
                    else
                    {
                        if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;
                    }
                }

                // 更新照相机位置.
                PositionCamera();

                MouseMovePoint = point;
            }
        }

        private void MainViewport3D_MouseUp(object sender, MouseButtonEventArgs e)
        {
            IsMouseDown = false;
        }
        #endregion

        #region 点击事件
        private void buttonUp_Click(object sender, RoutedEventArgs e)
        {
            CameraPhi += CameraDPhi;
            if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;

            // 更新照相机位置.
            PositionCamera();
        }

        private void buttonLeft_Click(object sender, RoutedEventArgs e)
        {
            CameraTheta += CameraDTheta;

            // 更新照相机位置.
            PositionCamera();
        }

        private void buttonDown_Click(object sender, RoutedEventArgs e)
        {
            CameraPhi -= CameraDPhi;
            if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;

            // 更新照相机位置.
            PositionCamera();
        }

        private void buttonRight_Click(object sender, RoutedEventArgs e)
        {
            CameraTheta -= CameraDTheta;

            // 更新照相机位置.
            PositionCamera();
        }
        #endregion

    }
}

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容