前面三个 WPF 专题我们学习了布局、控件、数据绑定、MVVM、自定义控件等核心知识。WPF 还有一个强大的功能------3D 图形支持。这一深度专题将带你进入 WPF 的 3D 世界,学习如何创建旋转的立方体、3D 模型、光照和相机控制。
前置知识要求:
已完成 WPF 专题一、二、三
了解基本的 3D 坐标概念(X、Y、Z 轴)
熟悉 C# 和 XAML 基础
4.1 WPF 3D 概述
4.1.1 WPF 3D 的核心组件
WPF 的 3D 功能位于 System.Windows.Media.Media3D 命名空间中。
text
┌─────────────────────────────────────────────────────────────────┐
│ WPF 3D 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Viewport3D │ │
│ │ (3D 场景容器,类似 2D 中的 Canvas) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Camera │ │ Model3D │ │ Lights │ │
│ │ (相机) │ │ (模型) │ │ (光源) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Geometry3D + │ │
│ │ Material │ │
│ │ (几何 + 材质) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.1.2 核心概念速览
| 组件 | 用途 | 类比 |
|---|---|---|
| Viewport3D | 3D 场景容器 | 2D 中的 Canvas |
| Camera | 观察视角 | 眼睛的位置和方向 |
| Model3D | 3D 物体 | 实体对象 |
| Geometry3D | 物体形状 | 物体的骨架 |
| Material | 材质外观 | 物体的皮肤、颜色、纹理 |
| Light | 光源 | 照明系统 |
| Transform3D | 变换 | 位置、旋转、缩放 |
4.2 第一个 3D 程序:旋转立方体
4.2.1 创建 3D 场景
xml
<Window x:Class="Wpf3DDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF 3D 立方体" Height="500" Width="700"
WindowStartupLocation="CenterScreen">
<Grid>
<!-- Viewport3D 是 3D 场景的根容器 -->
<Viewport3D x:Name="MainViewport">
<!-- 1. 相机:定义观察场景的视角 -->
<Viewport3D.Camera>
<PerspectiveCamera
x:Name="MainCamera"
Position="5,3,5" <!-- 相机位置 (X,Y,Z) -->
LookDirection="-5,-3,-5" <!-- 相机看向的方向 -->
UpDirection="0,1,0" <!-- 相机的"上"方向 -->
FieldOfView="45" <!-- 视野角度 -->
NearPlaneDistance="0.1" <!-- 近裁剪面 -->
FarPlaneDistance="100"/> <!-- 远裁剪面 -->
</Viewport3D.Camera>
<!-- 2. 光源:照亮场景 -->
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight Color="#404040"/> <!-- 环境光 -->
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Color="#FFFFFF"
Direction="-1,-1,-1"/> <!-- 方向光 -->
</ModelVisual3D.Content>
</ModelVisual3D>
<!-- 3. 模型:立方体 -->
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<!-- 几何定义:立方体的8个顶点和12个三角形 -->
<GeometryModel3D.Geometry>
<MeshGeometry3D
Positions="
-1, 1, 1 0 <!-- 前面:顶点0 -->
1, 1, 1 1 <!-- 顶点1 -->
1,-1, 1 2 <!-- 顶点2 -->
-1,-1, 1 3 <!-- 顶点3 -->
-1, 1,-1 4 <!-- 后面:顶点4 -->
1, 1,-1 5 <!-- 顶点5 -->
1,-1,-1 6 <!-- 顶点6 -->
-1,-1,-1 7" <!-- 顶点7 -->
TriangleIndices="
<!-- 前面 -->
0,1,2 0,2,3
<!-- 右面 -->
1,5,6 1,6,2
<!-- 后面 -->
5,4,7 5,7,6
<!-- 左面 -->
4,0,3 4,3,7
<!-- 上面 -->
4,5,1 4,1,0
<!-- 下面 -->
3,2,6 3,6,7"
TextureCoordinates="
0,0 1,0 1,1 0,1
0,0 1,0 1,1 0,1
0,0 1,0 1,1 0,1
0,0 1,0 1,1 0,1
0,0 1,0 1,1 0,1
0,0 1,0 1,1 0,1"/>
</GeometryModel3D.Geometry>
<!-- 材质:外观 -->
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Red"/>
</GeometryModel3D.Material>
<!-- 背面材质:背面可见时显示 -->
<GeometryModel3D.BackMaterial>
<DiffuseMaterial Brush="DarkRed"/>
</GeometryModel3D.BackMaterial>
<!-- 变换:位置、旋转、缩放 -->
<GeometryModel3D.Transform>
<Transform3DGroup>
<RotateTransform3D x:Name="CubeRotation">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0.5,1,0" Angle="0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
</Transform3DGroup>
</GeometryModel3D.Transform>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<!-- 辅助:坐标轴(可选)-->
<ModelVisual3D>
<ModelVisual3D.Content>
<!-- X 轴(红色)-->
<GeometryModel3D Geometry="{StaticResource ArrowGeometry}">
<GeometryModel3D.Material>
<DiffuseMaterial Brush="Red"/>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
</Window>
4.2.2 添加动画:让立方体旋转
csharp
// MainWindow.xaml.cs
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;
namespace Wpf3DDemo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// 启动动画
StartRotationAnimation();
}
private void StartRotationAnimation()
{
// 获取旋转对象
var rotation = (AxisAngleRotation3D)CubeRotation.Rotation;
// 创建角度动画
var animation = new DoubleAnimation
{
From = 0,
To = 360,
Duration = TimeSpan.FromSeconds(8),
RepeatBehavior = RepeatBehavior.Forever
};
// 应用到旋转角度
rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, animation);
}
}
}
4.3 3D 坐标系深入理解
4.3.1 坐标系统
text
Y 向上
│
│
│
│
└─────────► X 向右
/
/
/
Z 向屏幕外(右手坐标系)
WPF 使用右手坐标系:
- 拇指指向 X 正方向
- 食指指向 Y 正方向
- 中指指向 Z 正方向
4.3.2 相机详解
| 相机类型 | 说明 | 适用场景 |
|---|---|---|
| PerspectiveCamera | 透视相机(近大远小) | 大多数 3D 场景 |
| OrthographicCamera | 正交相机(无透视) | 2.5D 游戏、技术图纸 |
xml
<!-- 透视相机:有景深感 -->
<PerspectiveCamera
Position="5,5,5"
LookDirection="-5,-5,-5"
UpDirection="0,1,0"
FieldOfView="60"/>
<!-- 正交相机:平行投影,物体大小与距离无关 -->
<OrthographicCamera
Position="5,5,5"
LookDirection="-5,-5,-5"
UpDirection="0,1,0"
Width="10"/>
4.3.3 理解 LookDirection
csharp
// LookDirection = 从相机位置指向目标点的方向向量
// 公式:LookDirection = Target - Position
// 相机在 (5, 3, 5),看向原点 (0, 0, 0)
// LookDirection = (0 - 5, 0 - 3, 0 - 5) = (-5, -3, -5)
// 相机在 (0, 0, 10),看向原点
// LookDirection = (0, 0, -10)
// 简化:相机在 (0, 0, 10),看向 Z 轴负方向
<PerspectiveCamera Position="0,0,10" LookDirection="0,0,-1" UpDirection="0,1,0"/>
4.4 几何网格(MeshGeometry3D)
4.4.1 网格的基本概念
3D 模型由三角形组成。三角形是最小的平面单元,任何复杂形状都可以用三角形拼成。
text
一个矩形由 2 个三角形组成:
v0 ───── v1
│╲ │
│ ╲ │
│ ╲ │
│ ╲ │
v3 ───── v2
三角形1: v0, v1, v3
三角形2: v1, v2, v3
4.4.2 创建立方体的完整代码
csharp
public static MeshGeometry3D CreateCubeMesh(double size)
{
var mesh = new MeshGeometry3D();
double half = size / 2;
// 8 个顶点
Point3D[] vertices = new Point3D[]
{
new Point3D(-half, half, half), // 0: 前上左
new Point3D( half, half, half), // 1: 前上右
new Point3D( half, -half, half), // 2: 前下右
new Point3D(-half, -half, half), // 3: 前下左
new Point3D(-half, half, -half), // 4: 后上左
new Point3D( half, half, -half), // 5: 后上右
new Point3D( half, -half, -half), // 6: 后下右
new Point3D(-half, -half, -half), // 7: 后下左
};
foreach (var v in vertices)
mesh.Positions.Add(v);
// 12 个三角形(每个面 2 个三角形,共 6 个面)
int[,] triangles = new int[,]
{
// 前面 (Z = half)
{0, 1, 2}, {0, 2, 3},
// 右面 (X = half)
{1, 5, 6}, {1, 6, 2},
// 后面 (Z = -half)
{5, 4, 7}, {5, 7, 6},
// 左面 (X = -half)
{4, 0, 3}, {4, 3, 7},
// 上面 (Y = half)
{4, 5, 1}, {4, 1, 0},
// 下面 (Y = -half)
{3, 2, 6}, {3, 6, 7}
};
for (int i = 0; i < triangles.GetLength(0); i++)
{
mesh.TriangleIndices.Add(triangles[i, 0]);
mesh.TriangleIndices.Add(triangles[i, 1]);
mesh.TriangleIndices.Add(triangles[i, 2]);
}
return mesh;
}
4.4.3 纹理坐标(UV 映射)
csharp
// UV 坐标:0 到 1 之间,告诉纹理图片如何贴到三角形上
// (0,0) 是纹理左上角,(1,1) 是纹理右下角
// 为立方体的每个面设置 UV 坐标
public static void SetCubeTextureCoordinates(MeshGeometry3D mesh)
{
// 每个面 4 个顶点,对应 UV 坐标
Point[] uvs = new Point[]
{
// 前面
new Point(0,0), new Point(1,0), new Point(1,1), new Point(0,1),
// 右面
new Point(0,0), new Point(1,0), new Point(1,1), new Point(0,1),
// 后面
new Point(0,0), new Point(1,0), new Point(1,1), new Point(0,1),
// 左面
new Point(0,0), new Point(1,0), new Point(1,1), new Point(0,1),
// 上面
new Point(0,0), new Point(1,0), new Point(1,1), new Point(0,1),
// 下面
new Point(0,0), new Point(1,0), new Point(1,1), new Point(0,1)
};
foreach (var uv in uvs)
mesh.TextureCoordinates.Add(uv);
}
4.4.4 生成球体网格
csharp
public static MeshGeometry3D CreateSphereMesh(double radius, int segments)
{
var mesh = new MeshGeometry3D();
for (int lat = 0; lat <= segments; lat++)
{
double theta = lat * Math.PI / segments;
double sinTheta = Math.Sin(theta);
double cosTheta = Math.Cos(theta);
for (int lon = 0; lon <= segments; lon++)
{
double phi = lon * 2 * Math.PI / segments;
double sinPhi = Math.Sin(phi);
double cosPhi = Math.Cos(phi);
double x = radius * cosPhi * sinTheta;
double y = radius * cosTheta;
double z = radius * sinPhi * sinTheta;
mesh.Positions.Add(new Point3D(x, y, z));
// UV 坐标
double u = (double)lon / segments;
double v = (double)lat / segments;
mesh.TextureCoordinates.Add(new Point(u, v));
}
}
// 创建索引
for (int lat = 0; lat < segments; lat++)
{
for (int lon = 0; lon < segments; lon++)
{
int current = lat * (segments + 1) + lon;
int next = current + segments + 1;
mesh.TriangleIndices.Add(current);
mesh.TriangleIndices.Add(current + 1);
mesh.TriangleIndices.Add(next);
mesh.TriangleIndices.Add(next);
mesh.TriangleIndices.Add(current + 1);
mesh.TriangleIndices.Add(next + 1);
}
}
return mesh;
}
4.5 材质系统
4.5.1 材质类型对比
| 材质类型 | 说明 | 效果 |
|---|---|---|
| DiffuseMaterial | 漫反射材质 | 哑光效果,均匀反射光 |
| SpecularMaterial | 高光材质 | 有光泽,有高光点 |
| EmissiveMaterial | 自发光材质 | 自己发光,不受光照影响 |
| MaterialGroup | 材质组合 | 多种材质混合 |
xml
<!-- 漫反射材质:红砖效果 -->
<DiffuseMaterial Brush="Red"/>
<!-- 高光材质:金属效果 -->
<SpecularMaterial Brush="White" SpecularPower="50"/>
<!-- 自发光材质:霓虹灯效果 -->
<EmissiveMaterial Brush="Red"/>
<!-- 材质组 -->
<MaterialGroup>
<DiffuseMaterial Brush="Red"/>
<SpecularMaterial Brush="White" SpecularPower="30"/>
</MaterialGroup>
4.5.2 纹理材质(贴图)
xml
<!-- 从图片加载纹理 -->
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="/Images/BrickTexture.jpg"
TileMode="Tile"
Viewport="0,0,0.2,0.2"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
csharp
// 代码中设置纹理
public static DiffuseMaterial CreateTextureMaterial(string imagePath)
{
var bitmap = new BitmapImage();
bitmap.BeginInit();
bitmap.UriSource = new Uri(imagePath, UriKind.Relative);
bitmap.EndInit();
var brush = new ImageBrush(bitmap);
brush.TileMode = TileMode.Tile;
brush.Viewport = new Rect(0, 0, 0.2, 0.2);
brush.ViewportUnits = BrushMappingMode.RelativeToBoundingBox;
return new DiffuseMaterial(brush);
}
4.5.3 渐变材质
xml
<!-- 线性渐变 -->
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="Red" Offset="0"/>
<GradientStop Color="Blue" Offset="1"/>
</LinearGradientBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
<!-- 径向渐变 -->
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<RadialGradientBrush Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="Yellow" Offset="0"/>
<GradientStop Color="Orange" Offset="0.5"/>
<GradientStop Color="Red" Offset="1"/>
</RadialGradientBrush>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
4.6 光照系统
4.6.1 光源类型
| 光源类型 | 说明 | 效果 |
|---|---|---|
| AmbientLight | 环境光 | 均匀照亮所有面 |
| DirectionalLight | 方向光 | 如太阳,平行光线 |
| PointLight | 点光源 | 如灯泡,向四周发散 |
| SpotLight | 聚光灯 | 圆锥形光线 |
4.6.2 完整光源配置示例
xml
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<!-- 环境光:基础照明 -->
<AmbientLight Color="#404040"/>
<!-- 方向光:主光源 -->
<DirectionalLight Color="#FFFFFF" Direction="-1,-2,-1"/>
<!-- 点光源:暖色补光 -->
<PointLight Color="#FFAA66"
Position="2,1,3"
ConstantAttenuation="1"
LinearAttenuation="0.1"
QuadraticAttenuation="0.01"/>
<!-- 聚光灯:强调效果 -->
<SpotLight Color="#66AAFF"
Position="-2,3,-1"
Direction="1,-1,0"
InnerConeAngle="20"
OuterConeAngle="45"
ConstantAttenuation="1"/>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
4.6.3 动态光源(跟随相机)
csharp
public partial class MainWindow : Window
{
private PointLight _followLight;
public MainWindow()
{
InitializeComponent();
SetupFollowLight();
}
private void SetupFollowLight()
{
// 创建跟随相机移动的点光源
_followLight = new PointLight
{
Color = Colors.White,
ConstantAttenuation = 1,
LinearAttenuation = 0.1
};
// 添加到场景
var lightVisual = new ModelVisual3D();
lightVisual.Content = _followLight;
MainViewport.Children.Add(lightVisual);
// 绑定光源位置到相机位置
var binding = new Binding
{
Source = MainCamera,
Path = new PropertyPath("Position")
};
_followLight.SetBinding(PointLight.PositionProperty, binding);
}
}
4.7 变换系统
4.7.1 变换类型
| 变换类型 | 说明 | 应用场景 |
|---|---|---|
| TranslateTransform3D | 平移 | 移动物体 |
| RotateTransform3D | 旋转 | 绕轴旋转 |
| ScaleTransform3D | 缩放 | 放大/缩小 |
| MatrixTransform3D | 矩阵变换 | 复杂组合变换 |
4.7.2 组合变换示例
xml
<GeometryModel3D.Transform>
<Transform3DGroup>
<!-- 1. 旋转 -->
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0,1,0" Angle="45"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<!-- 2. 缩放 -->
<ScaleTransform3D ScaleX="1.5" ScaleY="1.5" ScaleZ="1.5"/>
<!-- 3. 平移 -->
<TranslateTransform3D OffsetX="2" OffsetY="1" OffsetZ="0"/>
</Transform3DGroup>
</GeometryModel3D.Transform>
4.7.3 键盘控制物体移动
csharp
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
private TranslateTransform3D _cubeTransform;
private double _moveSpeed = 0.2;
public MainWindow()
{
InitializeComponent();
// 获取立方体的平移变换
_cubeTransform = GetTranslateTransform(CubeModel);
// 注册键盘事件
this.KeyDown += MainWindow_KeyDown;
}
private void MainWindow_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
_cubeTransform.OffsetZ -= _moveSpeed;
break;
case Key.Down:
_cubeTransform.OffsetZ += _moveSpeed;
break;
case Key.Left:
_cubeTransform.OffsetX -= _moveSpeed;
break;
case Key.Right:
_cubeTransform.OffsetX += _moveSpeed;
break;
case Key.PageUp:
_cubeTransform.OffsetY += _moveSpeed;
break;
case Key.PageDown:
_cubeTransform.OffsetY -= _moveSpeed;
break;
}
// 更新相机看向的物体(可选)
UpdateCameraLookAt();
}
private TranslateTransform3D GetTranslateTransform(GeometryModel3D model)
{
var transformGroup = model.Transform as Transform3DGroup;
if (transformGroup != null)
{
foreach (var transform in transformGroup.Children)
{
if (transform is TranslateTransform3D translate)
return translate;
}
}
return new TranslateTransform3D();
}
}
4.8 3D 模型从文件加载
4.8.1 使用 HelixToolkit(推荐第三方库)
bash
# 安装 HelixToolkit(支持加载 OBJ、STL 等格式)
dotnet add package HelixToolkit.Wpf
xml
<Window xmlns:helix="http://helix-toolkit.org/wpf">
<Grid>
<!-- 使用 HelixViewport3D 替代 Viewport3D -->
<helix:HelixViewport3D x:Name="Viewport">
<!-- 加载 OBJ 文件 -->
<helix:FileModelVisual3D
x:Name="Model"
FileName="Models/teapot.obj"
Material="{StaticResource MetalMaterial}"/>
<!-- 添加地面网格 -->
<helix:GridLinesVisual3D
Width="20"
Length="20"
MajorStep="1"
MinorStep="0.2"/>
<!-- 添加坐标轴 -->
<helix:AxisVisual3D/>
</helix:HelixViewport3D>
</Grid>
</Window>
csharp
// 代码中加载模型
private void LoadModel(string filePath)
{
var importer = new HelixToolkit.Wpf.ModelImporter();
var model = importer.Load(filePath);
var visual = new ModelVisual3D();
visual.Content = model;
Viewport.Children.Add(visual);
}
4.9 交互式 3D
4.9.1 鼠标控制相机旋转
csharp
public class MouseCameraController
{
private PerspectiveCamera _camera;
private Point _lastMousePosition;
private double _rotationX = 30;
private double _rotationY = 45;
private double _distance = 10;
public void Attach(FrameworkElement element, PerspectiveCamera camera)
{
_camera = camera;
element.MouseLeftButtonDown += OnMouseDown;
element.MouseMove += OnMouseMove;
element.MouseWheel += OnMouseWheel;
}
private void OnMouseDown(object sender, MouseButtonEventArgs e)
{
_lastMousePosition = e.GetPosition((IInputElement)sender);
((IInputElement)sender).CaptureMouse();
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed) return;
var currentPos = e.GetPosition((IInputElement)sender);
var delta = currentPos - _lastMousePosition;
_rotationX += delta.Y * 0.5;
_rotationY += delta.X * 0.5;
UpdateCameraPosition();
_lastMousePosition = currentPos;
}
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
_distance -= e.Delta * 0.01;
_distance = Math.Max(3, Math.Min(20, _distance));
UpdateCameraPosition();
}
private void UpdateCameraPosition()
{
double radX = _rotationX * Math.PI / 180;
double radY = _rotationY * Math.PI / 180;
double x = _distance * Math.Sin(radY) * Math.Cos(radX);
double y = _distance * Math.Sin(radX);
double z = _distance * Math.Cos(radY) * Math.Cos(radX);
_camera.Position = new Point3D(x, y, z);
_camera.LookDirection = new Vector3D(-x, -y, -z);
}
}
4.9.2 鼠标拾取 3D 物体
csharp
private void Viewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
// 获取鼠标点击的 3D 位置
var hitParams = new PointHitTestParameters(e.GetPosition(MainViewport));
VisualTreeHelper.HitTest(MainViewport, null,
result =>
{
var rayHit = result as RayHitTestResult;
if (rayHit != null)
{
// 获取击中的模型
var model = rayHit.ModelHit as GeometryModel3D;
if (model != null)
{
// 高亮效果
HighlightModel(model);
}
}
return HitTestResultBehavior.Stop;
}, hitParams);
}
private void HighlightModel(GeometryModel3D model)
{
// 保存原始材质
_originalMaterial = model.Material;
// 设置为高亮材质
model.Material = new EmissiveMaterial(Brushes.Yellow);
// 创建恢复动画
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(0.5)
};
timer.Tick += (s, e) =>
{
model.Material = _originalMaterial;
timer.Stop();
};
timer.Start();
}
4.10 综合示例:旋转的地球仪
xml
<!-- EarthWindow.xaml -->
<Window x:Class="Wpf3DDemo.EarthWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="3D 地球仪" Height="600" Width="800"
WindowStartupLocation="CenterScreen">
<Window.Resources>
<!-- 地球纹理 -->
<ImageBrush x:Key="EarthTexture" ImageSource="/Images/earth_map.jpg"/>
<!-- 大气层材质 -->
<RadialGradientBrush x:Key="AtmosphereBrush" Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5">
<GradientStop Color="#00FFFFFF" Offset="0.8"/>
<GradientStop Color="#44FFFFFF" Offset="0.95"/>
<GradientStop Color="#00FFFFFF" Offset="1"/>
</RadialGradientBrush>
<!-- 地球材质 -->
<DiffuseMaterial x:Key="EarthMaterial" Brush="{StaticResource EarthTexture}"/>
<!-- 高光材质 -->
<SpecularMaterial x:Key="EarthSpecular" Brush="White" SpecularPower="30"/>
<!-- 组合材质 -->
<MaterialGroup x:Key="EarthMaterialGroup">
<StaticResource ResourceKey="EarthMaterial"/>
<StaticResource ResourceKey="EarthSpecular"/>
</MaterialGroup>
</Window.Resources>
<Grid>
<Viewport3D x:Name="MainViewport" ClipToBounds="True">
<!-- 相机 -->
<Viewport3D.Camera>
<PerspectiveCamera x:Name="Camera"
Position="0,0,5"
LookDirection="0,0,-5"
UpDirection="0,1,0"
FieldOfView="45"/>
</Viewport3D.Camera>
<!-- 光源 -->
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<AmbientLight Color="#333333"/>
<DirectionalLight Color="#FFFFFF" Direction="-1,-2,-1"/>
<PointLight Color="#FFAA66" Position="3,2,4"/>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
<!-- 地球模型 -->
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D x:Name="EarthModel">
<GeometryModel3D.Geometry>
<MeshGeometry3D x:Name="EarthMesh"/>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<StaticResource ResourceKey="EarthMaterialGroup"/>
</GeometryModel3D.Material>
<GeometryModel3D.Transform>
<RotateTransform3D x:Name="EarthRotation">
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0,1,0" Angle="0"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
</GeometryModel3D.Transform>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<!-- 大气层效果 -->
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D x:Name="AtmosphereMesh"/>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial Brush="{StaticResource AtmosphereBrush}"/>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
<!-- 星空背景 -->
<ModelVisual3D>
<ModelVisual3D.Content>
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-100,-100,100 100,-100,100 100,100,100 -100,100,100"
TriangleIndices="0,1,2 0,2,3"/>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<ImageBrush ImageSource="/Images/stars.jpg" TileMode="Tile"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
</GeometryModel3D>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
<!-- UI 控制面板 -->
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="20" Background="#80000000" Padding="15" CornerRadius="8">
<Slider x:Name="RotationSpeedSlider"
Minimum="0" Maximum="10" Value="5"
Width="200" Margin="0,0,0,10"/>
<Label Content="{Binding Value, ElementName=RotationSpeedSlider, StringFormat=转速: {0:F1}/秒}"
Foreground="White" HorizontalContentAlignment="Center"/>
<Slider x:Name="ZoomSlider"
Minimum="2" Maximum="15" Value="5"
Width="200"/>
</StackPanel>
</Grid>
</Window>
csharp
// EarthWindow.xaml.cs
using System.Windows;
using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;
using System.Windows.Threading;
namespace Wpf3DDemo
{
public partial class EarthWindow : Window
{
private DispatcherTimer _rotationTimer;
private double _currentAngle = 0;
public EarthWindow()
{
InitializeComponent();
// 创建地球网格
EarthMesh = CreateSphereMesh(1.0, 64);
AtmosphereMesh = CreateSphereMesh(1.02, 64);
// 启动自动旋转
StartAutoRotation();
// 绑定 UI 事件
RotationSpeedSlider.ValueChanged += RotationSpeed_ValueChanged;
ZoomSlider.ValueChanged += ZoomSlider_ValueChanged;
}
private MeshGeometry3D CreateSphereMesh(double radius, int segments)
{
var mesh = new MeshGeometry3D();
var vertices = new List<Point3D>();
var uvs = new List<Point>();
for (int lat = 0; lat <= segments; lat++)
{
double theta = lat * Math.PI / segments;
double sinTheta = Math.Sin(theta);
double cosTheta = Math.Cos(theta);
for (int lon = 0; lon <= segments; lon++)
{
double phi = lon * 2 * Math.PI / segments;
double sinPhi = Math.Sin(phi);
double cosPhi = Math.Cos(phi);
double x = radius * cosPhi * sinTheta;
double y = radius * cosTheta;
double z = radius * sinPhi * sinTheta;
vertices.Add(new Point3D(x, y, z));
uvs.Add(new Point((double)lon / segments, (double)lat / segments));
}
}
foreach (var v in vertices)
mesh.Positions.Add(v);
foreach (var uv in uvs)
mesh.TextureCoordinates.Add(uv);
// 创建索引
for (int lat = 0; lat < segments; lat++)
{
for (int lon = 0; lon < segments; lon++)
{
int current = lat * (segments + 1) + lon;
int next = current + segments + 1;
mesh.TriangleIndices.Add(current);
mesh.TriangleIndices.Add(current + 1);
mesh.TriangleIndices.Add(next);
mesh.TriangleIndices.Add(next);
mesh.TriangleIndices.Add(current + 1);
mesh.TriangleIndices.Add(next + 1);
}
}
return mesh;
}
private void StartAutoRotation()
{
_rotationTimer = new DispatcherTimer();
_rotationTimer.Interval = TimeSpan.FromMilliseconds(16); // ~60fps
_rotationTimer.Tick += (s, e) =>
{
double speed = RotationSpeedSlider.Value;
if (speed > 0)
{
_currentAngle += speed * 0.5; // 每帧增加角度
if (_currentAngle > 360) _currentAngle -= 360;
var rotation = (AxisAngleRotation3D)EarthRotation.Rotation;
rotation.Angle = _currentAngle;
}
};
_rotationTimer.Start();
}
private void RotationSpeed_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
// 速度变化时不需要额外处理,定时器会读取最新值
}
private void ZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
double zoom = ZoomSlider.Value;
Camera.Position = new Point3D(0, 0, zoom);
Camera.LookDirection = new Vector3D(0, 0, -zoom);
}
}
}
4.11 性能优化
4.11.1 减少三角形数量
csharp
// 对于不需要细节的物体使用较少的细分
// 近处物体高精度,远处物体低精度
public enum DetailLevel
{
Low, // 8x8 细分,512 三角形
Medium, // 32x32 细分,2048 三角形
High // 128x128 细分,32768 三角形
}
public static int GetSegments(DetailLevel level)
{
return level switch
{
DetailLevel.Low => 16,
DetailLevel.Medium => 32,
DetailLevel.High => 64,
_ => 32
};
}
4.11.2 冻结对象提高性能
csharp
// 冻结不会改变的对象,提高渲染性能
var mesh = CreateSphereMesh(1.0, 32);
mesh.Freeze(); // 冻结后不能再修改
var material = new DiffuseMaterial(Brushes.Red);
material.Freeze();
4.11.3 使用 GeometryModel3D 缓存
csharp
public class ModelCache
{
private static Dictionary<string, GeometryModel3D> _cache = new();
public static GeometryModel3D GetModel(string key, Func<GeometryModel3D> factory)
{
if (!_cache.ContainsKey(key))
{
var model = factory();
model.Freeze(); // 缓存后冻结
_cache[key] = model;
}
// 返回克隆(因为不能共享同一个实例)
return CloneModel(_cache[key]);
}
private static GeometryModel3D CloneModel(GeometryModel3D original)
{
return new GeometryModel3D
{
Geometry = original.Geometry,
Material = original.Material,
BackMaterial = original.BackMaterial,
Transform = original.Transform
};
}
}