WPF:3D球体

一、坐标系

  • WPF 的 3D 坐标系原点通常位于对象中心,X 轴向右,Y 轴向上,Z 轴朝向观察者(右手定则)。

二、球体网格生成(经纬度法)

  • 球体表面可以用二维参数方程表示,其中两个参数是纬度角 θ(-π/2 ~ π/2)和经度角 φ(0 ~ 2π)。
    对于给定的半径 R,球面上任意点的坐标为:

x = R · cos θ · cos φ
y = R · sin θ
z = R · cos θ · sin φ

  • 顶点排布:
    建立一个二维网格,stacks 表示纬度分段数(行数),slices 表示经度分段数(列数)。每个网格交点就是一个顶点。
    为了形成闭合球体,纬度循环从 0 到 stacks(包含两极),经度循环从 0 到 slices(包含首尾重合点,但顶点位置相同,以便纹理无缝)。

  • 三角形索引:
    每个四边形单元由两个三角形构成。对于第 i 行第 j 列的四边形,四个顶点索引为:

    • 左上:i * (slices + 1) + j
      右上:i * (slices + 1) + j + 1
      左下:(i+1) * (slices + 1) + j
      右下:(i+1) * (slices + 1) + j + 1
      两个三角形:(左上, 右上, 左下) 和 (右上, 右下, 左下)。
  • 法线计算:
    对于球心在原点的标准球体,法线方向就是顶点位置向量归一化后的方向(因为球面上任意点的径向方向就是该点的法线方向)。WPF 中需要显式提供法线数据,否则光照计算不准确。

  • 纹理坐标:
    将参数 u = φ/(2π) 映射到 [0,1],v = (θ + π/2)/π 映射到 [0,1],实现纹理贴图的 UV 坐标。

三、WPF 3D 渲染机制

  • Viewport3D 是 3D 场景的容器,内置了透视投影、裁剪等管线。
  • PerspectiveCamera 定义观察者的位置和方向。
  • 灯光:环境光提供基础照明,方向光产生立体感。
  • GeometryModel3D 包含网格几何(MeshGeometry3D)和材质(Material)。网格的顶点、法线、纹理坐标、三角形索引共同决定了模型的形状和光照表现。
  • Transform 通过对模型或整个 Visual 应用旋转、平移、缩放等变换实现动画。

四、动画实现

  • 可以使用 DispatcherTimer 定时更新 RotateTransform3D 的角度值,使球体绕 Y 轴匀速旋转。
    也可以使用 WPF 的 Storyboard 和 DoubleAnimation 实现更平滑的动画。


五、MainWindow.xaml

<Window x:Class="WpfA_Ball.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_Ball"
        mc:Ignorable="d"
        Title="3D 球体" Height="450" Width="600">
    <Grid>
        <Viewport3D Name="viewport3D">
            <!-- 相机:透视相机,观察位置在 (0,0,5) -->
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0,0,5" LookDirection="0,0,-1" UpDirection="0,1,0" FieldOfView="60" />
            </Viewport3D.Camera>
            <!-- 环境光 -->
            <Viewport3D.Children>
                <ModelVisual3D>
                    <ModelVisual3D.Content>
                        <AmbientLight Color="#404040" />
                    </ModelVisual3D.Content>
                </ModelVisual3D>
                <!-- 方向光 -->
                <ModelVisual3D>
                    <ModelVisual3D.Content>
                        <DirectionalLight Color="White" Direction="-1,-1,-1" />
                    </ModelVisual3D.Content>
                </ModelVisual3D>
                <!-- 球体模型(由代码动态生成) -->
                <ModelVisual3D x:Name="sphereVisual">
                    <ModelVisual3D.Content>
                        <GeometryModel3D x:Name="sphereModel" />
                    </ModelVisual3D.Content>
                </ModelVisual3D>
            </Viewport3D.Children>
        </Viewport3D>
    </Grid>
</Window>

六、MainWindow.xaml.cs

using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;
using System.Windows.Threading;


namespace WpfA_Ball
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            // 生成球体网格
            MeshGeometry3D mesh = CreateSphereMesh(radius: 1.0, slices: 30, stacks: 30);
            sphereModel.Geometry = mesh;

            // 设置材质(蓝色漫反射)
            MaterialGroup materialGroup = new MaterialGroup();
            materialGroup.Children.Add(new DiffuseMaterial(new SolidColorBrush(Colors.DodgerBlue)));
            // 添加高光
            materialGroup.Children.Add(new SpecularMaterial(new SolidColorBrush(Colors.White), 20));
            sphereModel.Material = materialGroup;

            // 启动旋转动画
            StartRotation();
        }

        /// <summary>
        /// 生成球体网格(经纬度法)
        /// </summary>
        private MeshGeometry3D CreateSphereMesh(double radius, int slices, int stacks)
        {
            var mesh = new MeshGeometry3D();
            var positions = new Point3DCollection();
            var normals = new Vector3DCollection();
            var texCoords = new PointCollection();
            var triangleIndices = new Int32Collection();

            // 顶点循环:纬度从 -π/2 到 π/2,经度从 0 到 2π
            for (int i = 0; i <= stacks; i++)
            {
                double theta = Math.PI * i / stacks - Math.PI / 2; // 纬度角
                double sinTheta = Math.Sin(theta);
                double cosTheta = Math.Cos(theta);

                for (int j = 0; j <= slices; j++)
                {
                    double phi = 2 * Math.PI * j / slices; // 经度角
                    double sinPhi = Math.Sin(phi);
                    double cosPhi = Math.Cos(phi);

                    // 顶点坐标
                    double x = radius * cosTheta * cosPhi;
                    double y = radius * sinTheta;
                    double z = radius * cosTheta * sinPhi;

                    positions.Add(new Point3D(x, y, z));

                    // 法线(单位向量,球心在原点时就是顶点方向)
                    // 法线 = 归一化的位置向量(因为球心在原点)
                    normals.Add(new Vector3D(x / radius, y / radius, z / radius));


                    // 纹理坐标 (u, v)
                    double u = (double)j / slices;
                    double v = (double)i / stacks;
                    texCoords.Add(new Point(u, v));
                }
            }

            // 生成三角形索引(每个四边形分成两个三角形)
            for (int i = 0; i < stacks; i++)
            {
                for (int j = 0; j < slices; j++)
                {
                    int current = i * (slices + 1) + j;
                    int next = current + slices + 1;

                    // 两个三角形构成一个四边形
                    triangleIndices.Add(current);
                    triangleIndices.Add(current + 1);
                    triangleIndices.Add(next);

                    triangleIndices.Add(current + 1);
                    triangleIndices.Add(next + 1);
                    triangleIndices.Add(next);
                }
            }

            mesh.Positions = positions;
            mesh.Normals = normals;
            mesh.TextureCoordinates = texCoords;
            mesh.TriangleIndices = triangleIndices;

            return mesh;
        }

        private void StartRotation()
        {
            // 创建一个旋转对象,并赋给 Visual 的 Transform
            var rotation = new AxisAngleRotation3D(new Vector3D(0, 1, 0), 0);
            sphereVisual.Transform = new RotateTransform3D(rotation);

            // 设置无限旋转动画
            var animation = new DoubleAnimation
            {
                From = 0,
                To = 360,
                Duration = TimeSpan.FromSeconds(10),
                RepeatBehavior = RepeatBehavior.Forever
            };

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

友情链接更多精彩内容