🎮 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), // 0new Point3D(0.5*width, -0.5*height, -0.5*depth),  // 1new Point3D(0.5*width, 0.5*height, -0.5*depth),   // 2new Point3D(-0.5*width, 0.5*height, -0.5*depth),  // 3new Point3D(-0.5*width, -0.5*height, 0.5*depth),  // 4new Point3D(0.5*width, -0.5*height, 0.5*depth),   // 5new Point3D(0.5*width, 0.5*height, 0.5*depth),    // 6new 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 应用》敬请期待!


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/diannao/89197.shtml
繁体地址,请注明出处:http://hk.pswp.cn/diannao/89197.shtml
英文地址,请注明出处:http://en.pswp.cn/diannao/89197.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Ruby 安装使用教程

一、Ruby 简介 Ruby 是一种简单快捷的面向对象脚本语言&#xff0c;以优雅、简洁、易读著称。它常被用于 Web 开发&#xff08;如 Ruby on Rails 框架&#xff09;、自动化脚本、DevOps、命令行工具等领域。 二、Ruby 安装教程 2.1 支持平台 Ruby 支持跨平台运行&#xff0c…

python | numpy小记(五):理解 NumPy 中的 `np.arccos`:反余弦函数

python | numpy小记&#xff08;五&#xff09;&#xff1a;理解 NumPy 中的 np.arccos&#xff1a;反余弦函数 一、函数签名与核心参数二、数学定义与取值范围三、基础使用示例四、与 Python 内建 math.acos 的对比五、常见问题与注意事项六、典型应用场景1. 三维向量夹角计算…

华为云Flexus+DeepSeek征文 | 华为云ModelArts与Reor的完美结合:创建高效本地AI笔记环境

华为云FlexusDeepSeek征文 | 华为云ModelArts与Reor的完美结合&#xff1a;创建高效本地AI笔记环境 引言一、ModelArts Studio平台介绍华为云ModelArts Studio简介ModelArts Studio主要特点 二、Reor介绍Reor简介Reor主要特点 三、安装Reor工具下载Reor软件安装Reor工具 四、开…

【启发式算法】Dynamic A*(D*)算法详细介绍(Python)

&#x1f4e2;本篇文章是博主人工智能&#xff08;AI&#xff09;领域学习时&#xff0c;用于个人学习、研究或者欣赏使用&#xff0c;并基于博主对相关等领域的一些理解而记录的学习摘录和笔记&#xff0c;若有不当和侵权之处&#xff0c;指出后将会立即改正&#xff0c;还望谅…

报告怎么写

替代方案&#xff08;按场景选择&#xff09; 岗前准备阶段 ✅ "熟悉业务流程/系统操作" ✅ "掌握XX工具/平台的核心功能" ✅ "完成上岗前技术对接" 知识转化场景 ✅ "梳理产品知识体系" ✅ "转化技术文档为实操方案" ✅ &…

大模型——怎么让 AI 写出好看有设计感的网页

大模型——怎么让 AI 写出好看有设计感的网页 你让 AI 给你写的网页大概都是这样的: 或者这样: 好点的时候能这样: 但都不够高级,尤其是那个像引用一样的边框,太 AI 了。 今天教大家一个小技巧,写出下面这样的网页: 或者这样的

【Torch】nn.Linear算法详解

1. 定义 nn.Linear 是 PyTorch 中最基础的全连接&#xff08;fully‐connected&#xff09;线性层&#xff0c;也称仿射变换层&#xff08;affine layer&#xff09;。它对输入张量做一次线性变换&#xff1a; output x W T b \text{output} x W^{T} b outputxWTb 其中&a…

ZGC收集器

ZGC收集器 欢迎来到我的博客&#xff1a;TWind的博客 我的CSDN:&#xff1a;Thanwind-CSDN博客 我的掘金&#xff1a;Thanwinde 的个人主页 0.前言 ZGC收集器完全可以说是Java收集器的一个跨时代的收集器&#xff0c;他真正意义上实现了停顿时间在10ms以内并且几乎全时段都是…

隧道技术篇2frp代理nps代理shisel代理

FRP代理 1.实现湖北内网控制北京的内网C2上线 2.实现湖北内网探针北京内网 信息收集 &#xff08;socks建立和端口映射&#xff09; 1.连接47.98.210.85 7000端口服务端 2.尝试将服务端的6666转到127.0.0.1 5555采用tcp协议&#xff0c;备注名proxies serverAddr"47.98…

[Python 基础课程]PyCharm 的安装

Python 的编辑器目前主流的有 PyCharm 和 Vscode。 PyCharm 是 Python 目前最主流、最常用、最推荐的 Python 编辑器。 https://www.jetbrains.com/pycharm/ PyCharm 有社区版和专业版&#xff0c;可以根据自己的需要下载对应的版本。社区版是收费的&#xff0c;对于初学者或…

Spread Ribbon 工具栏控件:在WinForms中高效编辑Spread工作簿

引言 在数据密集型应用中&#xff0c;电子表格功能是提升用户体验的关键要素。GrapeCity Spread.NET V17 推出的独立 Ribbon工具栏控件&#xff0c;为WinForms开发者提供了与Excel高度一致的UI交互体验。通过集成此控件&#xff0c;用户可直观地进行数据编辑、格式调整等操作&…

leedcode:找到字符串中所有字母异位词

问题&#xff1a;给定两个字符串 s 和 p&#xff0c;找到 s 中所有 p 的 异位词 的子串&#xff0c;返回这些子串的起始索引。不考虑答案输出的顺序。 package com.text;import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;…

华为云Flexus+DeepSeek征文|基于华为云 Flexus Dify平台开发AI Agent的详细流程

目录 一、Dify 应用开发平台 1.1 什么是 Dify&#xff1f; 1.2 Dify 平台优势 二、构建 AI Agent 2.1 创建智能客服助手 2.2 配置 LLM组件 三、访问智能应用 3.1 应用发布 3.2 智能对话 四、API 调用智能客服助手 4.1 配置 API 访问接口 4.2 调用智能客服助手API …

【知识图谱构建系列7】:结果评价(1)

文章目录 前言前情提要三元组提取结果评价脚本代码分析几分钟后前言 翻了一下记录,发现咱的知识图谱构建已经接近10天没有搞了。时间也是过得真快啊。但这毕竟是咱未来产生论文的主要阵地,所以得赶紧把节奏给拾起来哈~ 前情提要 我们已经可以在mistral模型的基础上,跑通提…

BT下载工具 qBittorrent v5.1.1.10,便携无广告,BT下载速度翻倍

[软件名称]: BT下载工具 qBittorrent v5.1.1.10 [软件大小]: 15.9 MB [下载通道]: 夸克盘 | 迅雷盘 软件介绍 &#x1f525;《qBittorrent增强版》v5.1.1.10便携版&#xff5c;BT下载神器&#xff0c;速度与隐私兼得&#x1f310; ✨ 核心优势&#xff1a; ✅ 无视版权限制…

裂变与重构:2025年大模型生态全景透视及未来趋势研判

1. 2025上半年&#xff1a;大模型生态的裂变时刻 1.1 技术范式革命&#xff1a;从生成到推理的跨越 2025年1月DeepSeek的横空出世&#xff0c;标志着大模型正式进入"推理时代"。这款国产模型在发布首周即突破1亿用户量级&#xff0c;其核心突破在于将传统生成能力升…

【docker】如何正确拉取langgraph-api

加这些配置都没用 # 设置代理环境变量 export HTTP_PROXY=http://127.0.0.1:7890 export HTTPS_PROXY=http://127.0.0.1:7890 # 设置更长的超时时间 export DOCKER_CLIENT_TIMEOUT=

PIXHAWK(ardupilot4.52)上传航点的bug

起因是查看飞控日志时发现地面站上传的平行航线&#xff0c;在日志看到航线却并不是平行的。 然后对比了一下地面站上传的航点信息跟飞控读取到的航点信息 发现经纬度只有前几位能够对应上&#xff0c;后几位都对应不上&#xff0c;两个点之间相差了50公分。地面站工程师认为地…

车载ECU刷写文件格式汇总详解

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 做到欲望极简&#xff0c;了解自己的真实欲望&#xff0c;不受外在潮流的影响&#xff0c;不盲从&#x…

Redis核心知识详解:从全局命令到高级数据结构

一、Redis全局命令详解 1.1 键查看与管理 dbsize&#xff1a;高效获取键总数&#xff08;O(1)操作&#xff09; 127.0.0.1:6379> dbsize (integer) 8 keys&#xff1a;生产环境避免使用&#xff08;O(n)操作&#xff09; # 查找user开头的键&#xff08;不推荐生产使用…