WPF 3D 开发全攻略:实现3D模型创建、旋转、平移、缩放

发布于:2025-06-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

🎮 WPF 3D 入门实战:从零打造一个可交互的立方体模型

标题:
🚀《WPF 3D 开发全攻略:实现旋转、平移、缩放与法线显示》


💡 引言

在现代图形应用中,3D 可视化已经成为不可或缺的一部分。WPF 提供了强大的 Viewport3D 控件,支持我们在桌面应用中轻松构建三维场景。

本文将手把手带你实现一个完整的 WPF 3D 应用程序,包括:

  • 创建基础立方体模型
  • 添加光源和材质
  • 实现鼠标控制视角(旋转、平移、缩放)
  • 法线计算与可视化展示
  • 完整项目结构与代码解析

非常适合刚接触 WPF 3D 的开发者入门学习!


🧱 第一步:创建窗口与视口

我们使用 Viewport3D 来承载所有 3D 内容,并为其添加相机和光源。

✅ XAML 部分(MainWindow.xaml)

<Window x:Class="_3D_WPF_Demo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="3D 立方体交互演示" Height="900" Width="1600">
    <Grid>
        <Viewport3D Name="viewPort"
                     MouseLeftButtonDown="ViewPort_MouseLeftButtonDown"
                     MouseLeftButtonUp="ViewPort_MouseLeftButtonUp"
                     MouseMove="ViewPort_MouseMove"
                     PreviewMouseWheel="ViewPort_PreviewMouseWheel"
                     MouseDown="ViewPort_MouseDown"
                     MouseUp="ViewPort_MouseUp">
            <!-- 环境光 -->
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <Model3DGroup>
                        <AmbientLight Color="White"/>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>

            <!-- 方向光 -->
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <Model3DGroup>
                        <DirectionalLight Color="White" Direction="0,1,0"/>
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>

            <!-- 相机 -->
            <Viewport3D.Camera>
                <PerspectiveCamera x:Name="mainCamera"
                                   Position="0,0,300"
                                   LookDirection="0,0,-1"
                                   UpDirection="0,1,0"
                                   FieldOfView="60"/>
            </Viewport3D.Camera>
        </Viewport3D>
    </Grid>
</Window>

📦 第二步:创建立方体模型

我们手动定义立方体顶点和三角形索引,并通过工具类生成几何体。

🧰 工具类 _3DModelOperateHelper.cs

using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace _3D_WPF_Demo.Tools
{
    public static class _3DModelOperateHelper
    {
        public static GeometryModel3D CreateSingleColorCube(int width, int height, int depth, DiffuseMaterial diffuseMaterial)
        {
            Point3DCollection points = new Point3DCollection(new Point3D[] {
                new Point3D(-0.5*width, -0.5*height, -0.5*depth), // 0
                new Point3D(0.5*width, -0.5*height, -0.5*depth),  // 1
                new Point3D(0.5*width, 0.5*height, -0.5*depth),   // 2
                new Point3D(-0.5*width, 0.5*height, -0.5*depth),  // 3
                new Point3D(-0.5*width, -0.5*height, 0.5*depth),  // 4
                new Point3D(0.5*width, -0.5*height, 0.5*depth),   // 5
                new Point3D(0.5*width, 0.5*height, 0.5*depth),    // 6
                new Point3D(-0.5*width, 0.5*height, 0.5*depth)    // 7
            });

            Int32Collection triangles = new Int32Collection(new int[] {
                // 底面 (Z-)
                0, 1, 2, 2, 3, 0,
                // 顶面 (Z+)
                4, 5, 6, 6, 7, 4,
                // 前面 (Y+)
                3, 2, 6, 6, 7, 3,
                // 后面 (Y-)
                0, 1, 5, 5, 4, 0,
                // 右面 (X+)
                1, 2, 6, 6, 5, 1,
                // 左面 (X-)
                4, 7, 3, 3, 0, 4
            });

            MeshGeometry3D mesh = new MeshGeometry3D();
            mesh.Positions = points;
            mesh.TriangleIndices = triangles;
            mesh.Normals = CalculateNormals(mesh); // 计算法线
            mesh.Freeze();

            return new GeometryModel3D
            {
                Geometry = mesh,
                Material = diffuseMaterial,
                BackMaterial = diffuseMaterial
            };
        }

        private static Vector3DCollection CalculateNormals(MeshGeometry3D mesh)
        {
            var normals = new Vector3DCollection();

            for (int i = 0; i < mesh.TriangleIndices.Count; i += 3)
            {
                int i1 = mesh.TriangleIndices[i];
                int i2 = mesh.TriangleIndices[i + 1];
                int i3 = mesh.TriangleIndices[i + 2];

                Point3D p1 = mesh.Positions[i1];
                Point3D p2 = mesh.Positions[i2];
                Point3D p3 = mesh.Positions[i3];

                Vector3D v1 = p2 - p1;
                Vector3D v2 = p3 - p1;

                Vector3D normal = Vector3D.CrossProduct(v1, v2);
                normal.Normalize();

                normals.Add(normal);
                normals.Add(normal);
                normals.Add(normal);
            }

            return normals;
        }
    }
}

🖱️ 第三步:实现交互操作

MainWindow.xaml.cs 中实现鼠标控制相机旋转、平移和缩放功能。

🧩 主要逻辑(MainWindow.xaml.cs)

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace _3D_WPF_Demo
{
    public partial class MainWindow : Window
    {
        private bool isDragging = false;
        private Point lastMousePosition;
        private double cameraDistance = 300;
        private Point3D centerPoint = new Point3D(0, 0, 0);
        private bool isPanning = false;
        private Point lastPanMousePosition;
        private Vector3D panOffset = new Vector3D(0, 0, 0);
        private ModelVisual3D modelVisual;
        private TranslateTransform3D modelTranslation = new TranslateTransform3D();
        private double yaw = 0;
        private double pitch = 0;

        public MainWindow()
        {
            InitializeComponent();

            viewPort.LostMouseCapture += (s, e) =>
            {
                isDragging = false;
                isPanning = false;
                Mouse.Capture(null);
            };

            LoadModel();
            UpdateCamera();
        }

        private void LoadModel()
        {
            var material = new DiffuseMaterial(new SolidColorBrush(Colors.Orange));
            GeometryModel3D model = _3DModelOperateHelper.CreateSingleColorCube(10, 10, 10, material);

            modelVisual = new ModelVisual3D();
            modelVisual.Content = model;
            modelVisual.Transform = modelTranslation;

            viewPort.Children.Add(modelVisual);

            if (model.Geometry is MeshGeometry3D mesh)
            {
                var normalGroup = new Model3DGroup();
                AddNormalLines(mesh, normalGroup);

                var normalVisual = new ModelVisual3D { Content = normalGroup };
                viewPort.Children.Add(normalVisual);
            }
        }

        private void AddNormalLines(MeshGeometry3D mesh, Model3DGroup group)
        {
            if (mesh.Positions == null || mesh.TriangleIndices == null || mesh.Normals == null)
                return;

            int indexCount = mesh.TriangleIndices.Count;
            for (int i = 0; i < indexCount; i += 3)
            {
                int i1 = mesh.TriangleIndices[i];
                int i2 = mesh.TriangleIndices[i + 1];
                int i3 = mesh.TriangleIndices[i + 2];

                Point3D p1 = mesh.Positions[i1];
                Point3D p2 = mesh.Positions[i2];
                Point3D p3 = mesh.Positions[i3];

                Point3D center = new Point3D(
                    (p1.X + p2.X + p3.X) / 3,
                    (p1.Y + p2.Y + p3.Y) / 3,
                    (p1.Z + p2.Z + p3.Z) / 3);

                Vector3D normal = mesh.Normals[i / 3];
                Point3D endPoint = center + normal * 5;

                var lineMesh = new MeshGeometry3D();
                lineMesh.Positions.Add(center);
                lineMesh.Positions.Add(endPoint);
                lineMesh.TriangleIndices.Add(0);
                lineMesh.TriangleIndices.Add(1);
                lineMesh.TriangleIndices.Add(1);

                var material = new DiffuseMaterial(Brushes.Magenta);
                var model = new GeometryModel3D(lineMesh, material);
                group.Children.Add(model);
            }
        }

        private void ViewPort_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            isDragging = true;
            lastMousePosition = e.GetPosition(viewPort);
            Mouse.Capture(viewPort);
        }

        private void ViewPort_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            isDragging = false;
            Mouse.Capture(null);
        }

        private void ViewPort_MouseMove(object sender, MouseEventArgs e)
        {
            var current = e.GetPosition(viewPort);
            if (isDragging)
            {
                Vector delta = current - lastMousePosition;
                yaw += delta.X * 0.3;
                pitch -= delta.Y * 0.3;
                UpdateCamera();
                lastMousePosition = current;
            }
            else if (isPanning)
            {
                UpdateCamera();
                lastPanMousePosition = current;
            }
        }

        private void ViewPort_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
        {
            double zoomFactor = e.Delta > 0 ? 1.1 : 0.9;
            cameraDistance *= zoomFactor;
            cameraDistance = Math.Max(50, Math.Min(cameraDistance, 1000));
            UpdateCamera();
        }

        private void ViewPort_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Middle)
            {
                isPanning = true;
                lastPanMousePosition = e.GetPosition(viewPort);
                Mouse.Capture(viewPort);
                e.Handled = true;
            }
        }

        private void ViewPort_MouseUp(object sender, MouseButtonEventArgs e)
        {
            if (e.ChangedButton == MouseButton.Left)
            {
                isDragging = false;
                Mouse.Capture(null);
            }

            if (e.ChangedButton == MouseButton.Middle)
            {
                isPanning = false;
                Mouse.Capture(null);
                e.Handled = true;
            }
        }

        private void UpdateCamera()
        {
            Vector3D initialLookDirection = new Vector3D(0, 0, -1);
            Matrix3D rotationMatrix = new Matrix3D();
            rotationMatrix.Rotate(new Quaternion(new Vector3D(0, 1, 0), yaw));
            rotationMatrix.Rotate(new Quaternion(new Vector3D(1, 0, 0), pitch));

            Vector3D rotatedLookDirection = rotationMatrix.Transform(initialLookDirection);
            mainCamera.Position = new Point3D(
                centerPoint.X - rotatedLookDirection.X * cameraDistance,
                centerPoint.Y - rotatedLookDirection.Y * cameraDistance,
                centerPoint.Z - rotatedLookDirection.Z * cameraDistance);

            mainCamera.LookDirection = rotatedLookDirection;
            mainCamera.UpDirection = new Vector3D(0, 1, 0);

            Vector3D forward = rotatedLookDirection;
            forward.Normalize();

            Vector3D right = Vector3D.CrossProduct(new Vector3D(0, 1, 0), forward);
            if (right.Length == 0) right = new Vector3D(1, 0, 0);
            right.Normalize();

            Vector3D up = Vector3D.CrossProduct(forward, right);
            up.Normalize();

            if (isPanning)
            {
                double panSpeed = 0.1;
                var delta = lastPanMousePosition - Mouse.GetPosition(viewPort);
                panOffset += right * (panSpeed * delta.X);
                panOffset += up * (panSpeed * delta.Y);
                modelTranslation.OffsetX = panOffset.X;
                modelTranslation.OffsetY = panOffset.Y;
                modelTranslation.OffsetZ = panOffset.Z;
                lastPanMousePosition = Mouse.GetPosition(viewPort);
            }
        }
    }
}

🎨 功能总结

功能 描述
🧱 立方体建模 手动定义顶点与三角形索引
🔆 光源设置 环境光 + 方向光增强视觉效果
🖱️ 鼠标交互 支持左键旋转、中键平移、滚轮缩放
🧭 相机控制 使用四元数进行相机旋转,避免万向节死锁
📐 法线可视化 绘制每个面的法线方向,便于理解光照原理

📚 小结与展望

本项目是一个非常完整的 WPF 3D 入门示例,涵盖了从模型创建到交互控制的核心知识点。你可以在此基础上继续扩展:

  • 加载 .obj.fbx 模型文件
  • 添加 UI 控件切换显示/隐藏法线
  • 使用 Helix Toolkit 实现更高级的功能
  • 实现动画、碰撞检测、粒子系统等进阶功能

📢 结语

如果你是 WPF 初学者或想了解 3D 编程,这个项目是非常好的起点。它不仅展示了如何使用 Viewport3DMedia3D,还帮助你掌握三维空间中的基本变换和交互技巧。

📌 下期预告:《WPF 3D 进阶篇:使用 Helix Toolkit 快速开发 3D 应用》敬请期待!


如需下载项目源码,关注群名片,我会提供压缩包链接。欢迎点赞、收藏、转发分享给更多学习者!👏


网站公告

今日签到

点亮在社区的每一天
去签到