一、坐标系
- 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
两个三角形:(左上, 右上, 左下) 和 (右上, 右下, 左下)。
- 左上:i * (slices + 1) + j
法线计算:
对于球心在原点的标准球体,法线方向就是顶点位置向量归一化后的方向(因为球面上任意点的径向方向就是该点的法线方向)。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);
}
}
}
