WPF 3D 開發全攻略:實現3D模型創建、旋轉、平移、縮放

🎮 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;不推薦生產使用…