C# 零基础到精通教程 - WPF 深度专题:3D 图形与视觉增强

前面三个 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
        };
    }
}
相关推荐
zhangfeng11332 小时前
台大李宏毅老师讲解memba和类似linear atttenion 模型,笔记
开发语言·人工智能·笔记
Chris _data3 小时前
并发单词频率统计器 - 从零到完整实现(C# 实战)
开发语言·c#
idolao3 小时前
Oligo 7.60 安装教程:引物设计+Java 环境配置
java·开发语言
不知名的老吴3 小时前
Lambda表达式与新的Streams API相结合
开发语言·python
石山代码10 小时前
ArrayList / HashMap / ConcurrentHashMap
java·开发语言
程序大视界10 小时前
【Python系列课程】Python正则表达式(下):环视、命名分组与日志实战
开发语言·python·正则表达式
枫叶v.11 小时前
Agent 分层存储架构设计:从记忆方法到中间件选型
开发语言·python
sleven fung12 小时前
MinerU与BabelDOC与KTransformers与OpenAI API库
开发语言·python·ai·langchain
萤萤七悬12 小时前
【Python笔记】AI帮实现CLI工具-使用argparse.ArgumentParser接收命令参数
开发语言·笔记·python