在WPF中使用HelixToolKit.Wpf.SharpDX工具包实现3D模型2D扁平化实战案例
前提声明:无论这篇文章是好是坏,我都不希望这篇文章变成付费的,仅作为个人开发时遇到的技术难题,提供一种解决方案,希望可以帮助到需要的人。
文本使用的项目资源:夸克网盘,无提取码
链接:https://pan.quark.cn/s/85160e69ac0d
问题引入一下
我在使用WPF开发一款类似WallPaperEngine的壁纸编辑器,又同时借鉴了冰与火之舞这款游戏的场景编辑器的设计思想。
具体来说,用户可以通过导入图片,将图片作为3D模型放入到3D视口中,利用透视相机(PerspectiveCamera),实现不同Z坐标的图片具有近大远小的距离感。
可以看一下我开发的软件原型示例
图一
解释一下:只是截取了部分界面,右侧的设置参数区域并负责打开图片,设置完3D模型参数,最后放置到左侧的3D视口(Viewport3D)。
你应该已经看到了,我先放置了一个6角圆角星图片(PNG格式,除了白色部分其余都是透明的),后放置了一个渐变色图片,前者距离相机更近一些。但是前面的模型按理来说透明的部分,应该是可以透过去看到后面被遮挡的渐变色图片的部分的,实则不然,6角圆角星图片遮挡了后面的图片,透明部分直接透到了视口的背景色。
解决方案
首先你需要了解一些信息,那就是WPF的3D视口和HelixToolkit的WPF原生版以及本文标题提到的HelixToolKit.Wpf.SharpDX版本的3D视口渲染原理和差别。
如果你清楚WPF的3D视口的渲染逻辑以及Helix包的渲染逻辑,这部分可以不看,直接看项目实战部分。
不要担心,我不会说复杂的底层渲染逻辑,因为我也不会。我只从感性的角度简要说明我的理解,或许会有偏差,如有错误请行内专业人士指正。
解决信息差
WPF的3D视口底层渲染逻辑
WPF的<Viewport3D>控件,采用 操纵GPU的技术:DirectX,WPF使用DirectX9,算是比较陈旧的版本了,这也意味着WPF的3D视口功能和渲染上都比较受限。
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Visual3DCollection Children => (Visual3DCollection)GetValue(ChildrenProperty);
这个是Viewport3D控件的源码,里面有一个Children属性,就是它来保存我们放置的模型。
你可以看到,他是一个数组(Collection)。所以你有必要知道WPF是怎么通过它来渲染的:
其实逻辑很简单:我们放入模型,就是直接在Children后面插入一个模型(Visual3D),WPF采用从数组最后一个元素开始绘制,最后绘制第一个元素。
其实也好理解,在WPF原生的3D视口中,所有模型,包括Light,也属于Visual3D的部分,所以,你放置的直射光也会被插入到Children数组中。那当然了,光照一般都会第一个放入视口,不然没有光你怎么看到模型?
<Viewport3D Grid.Column="1" x:Name="_ViewPortForGame" ClipToBounds="True">
<Viewport3D.Camera>
<PerspectiveCamera Position="{Binding CameraPosition}"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Direction="0,0,-1" Color="White"/>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
所以,为了保证所有模型都要被光照射,光要最后渲染,所以第一个模型一定是最后才渲染的。
所以你才会看到文章开始的问题,6角圆角星图片遮挡了后面的图片,透明部分直接透到了视口的背景色,就是因为6角圆角星是Children[1],渐变色图片是Children[2],所以先渲染了Children[2],后渲染了Children[1],我一开始使用的是漫反射材质(DiffuseMaterial),所以出现了这种不符合现实的效果。(后来我试过了使用别的WPF原生材质,比如SpecularMaterial (高光材质)虽然这种材质可以实现渐变色背景不被遮挡,但是效果不是很理想,这种材质使用了简单的像素加法,如果模型叠加,像素值会做加法,最大为255,这就导致,很多个3D模型一叠加,会有一大块白色的地方,而不是正确显示每个模型)
HelixTookKit的底层渲染原理
你需要下载对应的拓展包:

图二
HelixToolkit基于WPF原生的视口,封装了很多使用的功能,比如坐标轴显示,相机的详细信息卡片,视角立方体。还有许多封装好的3D模型(如Teaport一个茶壶),读者可以自行探索。
该包基于WPF原生的3D视口开发,所以渲染逻辑和WPF原生相同,而且,并没有提供更多的材质。
但是这并不妨碍你使用HelixToolkit这个包进行3D模型开发,相信很多业内人士都会使用这个包进行工业3D建模等用途。因为这个包在原生的基础上做了许多性能优化,同时提供了许多快捷键手势的接口,你可以想玩游戏一样,移动你的视角。而且使用了线性插值算法,让你的移动更丝滑。
总之,HelixToolkit包很常用,但是并不能解决我们的问题。
HelixToolkit.Wpf.SharpDX包的底层渲染逻辑
首先,你需要安装这些包:

图三
总共5个,可能有些不是必须,但是装上最好。
我把官方项目文档放在这里http://www.helix-toolkit.org
那么这个包做了什么呢?
1. 渲染引擎变化
这个包的版本使用了SharpDX工具。主要采用了DirectX11版本,是比较靠前的版本。
你可能会担心,你的电脑会不会不支持DirectX11的API。你可以查看自己电脑的DirectX配置,具体方法如下:
Windows操作系统:键入WIN+R,输入dxdiag

图四
然后会弹出相关配置:

图五
看到你的“DirectX版本”就知道你电脑的图形渲染API版本了。
DirectX11有许多更为强大的渲染算法,在本文主要使用了其中的顺序无关透明度 (Order Independent Transparency,OIT)。
下面我给出Deepseek对此的介绍:
🤔 为什么需要 OIT?
传统的 3D 渲染在处理透明物体时,要求开发者严格按照从后往前的顺序来渲染它们,这样颜色混合才会正确。例如,你必须先画后面的山,再画前面的透明玻璃。
但是,当场景变得复杂,或者物体自身有交叉(比如互相缠绕的丝带)时,手动排序就变得非常困难甚至不可能。一旦顺序出错,就会出现“透过玻璃看到的不是山,而是天空盒”这类视觉错误,这正是你最初遇到的那个“Bug”。
而 OIT 的核心目的,就是为了打破这种“必须按顺序渲染”的约束。它能让 GPU 在渲染时,不依赖于我们指定的渲染顺序,也能最终计算出正确的透明混合效果。
⚙️ OIT 是怎么做到的?(不同流派)
你可以简单理解:有各种不同流派的技术来实现 OIT,而
DepthPeeling只是其中之一。以下是几种主流的 OIT 实现方法对比:深度剥离 (Depth Peeling) 🧅
简单理解: 像剥洋葱一样,一层层“剥离”出距离相机最近的透明层,然后从后往前混合它们。
运行时多 Pass: ✅ 是 (N层 → N个Pass)
硬件要求: DX9
优点: 算法直观,结果精确,对硬件要求低。
缺点: 性能开销巨大,每剥离一层都需要重新渲染一遍整个场景。
适用场景: 对渲染质量要求极高但对性能要求不高的场景(如 CAD 模型预览)。
逐像素链表 (Per-Pixel Linked Lists) 🔗
简单理解: 为屏幕上的每个像素都创建一个“小本本”,把所有相关的透明片元都记录下来,最后一起排序混合。
运行时多 Pass: ❌ 否 (2 Pass)
硬件要求: DX11+
优点: 性能远比深度剥离高,因为只完整渲染场景一次。
缺点: 需要对 GPU 的显存消耗进行预估算,可能会占用较多显存。
适用场景: 适合大多数现代 3D 应用。
加权混合 (Weighted Blended OIT) ⚖️
简单理解: 用一套复杂的数学公式“估算”出最终的透明颜色,而不是精确计算。
运行时多 Pass: ❌ 否 (1 Pass)
硬件要求: DX11+
优点: 性能最高,只需一次渲染就能完成。
缺点: 结果是近似值,对于层次非常复杂的透明物体可能会出现视觉瑕疵。
适用场景: 对性能有极致要求的场景,如游戏特效。
2. API简要介绍
这里全是我的个人理解啊,可以不做标准参考,但是可能会对你的理解有一些帮助。
或许你接触过Hlsl语言,在WPF中,你有时会有自己写ShaderEffect来实现更为好看的效果。那么如何写自己写ShaderEffect呢?就需要用到Hlsl语言,本文不介绍,读者可以自己去查看别的文章或者AI搜索。
关于Hlsl语言,如果你了解了,你就知道,你会在开头先声明一个采样器(Sampler)
sampler2D Input : register(s0);
float4 main(float2 uv : TEXCOORD) : COLOR
{
float4 color = tex2D(Input, uv);
float gray = color.r * 0.299 + color.g * 0.587 + color.b * 0.114;
return float4(gray, gray, gray, color.a);
}
这是一个实现灰度效果的Hlsl代码,你不必看懂这些代码,你只需要知道他干了什么。
1. sampler2D Input : register(s0); 这个就是采样器,就类似于你把你的图像,或者控件,或者3D模型,作为Input,被hlsl采样,说白了就是输入你的被渲染对象。
2. float4 main(float2 uv : TEXCOORD) : COLOR : 这个是hlsl的入口函数,它使用uv坐标。比方说,你的图片,为1800x1000的大小,他会把你的图片归一化,从一个宽1800px,高1000的矩形变换到1x1的正方形,这么做就是为了统一坐标表示。
更专业的说法: uv 是纹理映射的坐标指针,由模型顶点的 UV 数据经过光栅化插值后自动提供给像素着色器,用于从纹理中提取颜色。
上面的没看懂也没关系,总之你需要意识到SharpDX使用了采样器和uv变换。
这也是它和WPF原生的3D视口不同的地方,因为他们底层的渲染逻辑完全不同。
| 特性 | WPF 原生 (Media3D) | SharpDX (如 HelixToolkit.Wpf.SharpDX) |
|---|---|---|
| 渲染 API | 基于 DirectX 9 (早期) / DirectX 9Ex(兼容 DX11 部分特性) | 直接使用 DirectX 11/12 |
| 渲染模式 | 保留模式:构建 Visual Tree → MILCore 编译成 DX 命令 → GPU | 即时模式:通过 SharpDX 直接调用 DX11 API,每帧提交绘制指令 |
| 透明混合 | 依赖物体渲染顺序,手动排序,无内置 OIT | 支持 OIT (Depth Peeling, Weighted Blended),自动处理复杂透明 |
| 材质系统 | 简单材质(Diffuse, Specular, Emissive) | 高级材质(Phong, PBR, Unlit),支持自定义 Shader |
| 性能 | 低(受限于 WPF 渲染管道,大量 Draw Call 开销) | 高(接近原生 DX11 性能,支持实例化、延迟渲染) |
| 适用场景 | 轻度 3D 展示(柱状图、简单模型) | 复杂 3D 场景(CAD、GIS、游戏、科学可视化) |
项目案例
相信你看过前面的介绍,或许对这个包有了感性的认识,接下来我会提供实际的例子来解决开篇遇到的问题。
1. 新建WPF项目
2. 安装NuGet包,参考上文[图三]。
添加包引用:xmlns:hdx="http://helix-toolkit.org/wpf/SharpDX"(注意一定是hdx这个名字,官方对此的约定,非SharpDX的Helix包引用时,名字写“hx”)
<Window x:Class="SharpDX渲染框架使用示例.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hdx="http://helix-toolkit.org/wpf/SharpDX"
mc:Ignorable="d"
Title="3D图片放置工具" Height="600" Width="900"
Closing="Window_Closing"
Loaded="Window_Loaded">
<Grid>
...
</Grid>
</Window>
3. 构建UI(本文的UI使用AI编写)
图六
4. 代码需要注意的地方
(1)内存管理
值得注意的是:在Xaml中,声明3D视口的Xaml代码:
<hdx:Viewport3DX x:Name="MyViewport"
Grid.Column="1"
BackgroundColor="#222222"
ShowCoordinateSystem="True"
ShowFrameRate="True"
ShowCameraInfo="True"
OITRenderMode="DepthPeeling"
EnableDeferredRendering="True"
EnableSwapChainRendering="False"
CoordinateSystemLabelForeground="AliceBlue"
EffectsManager="{Binding EffectsManager}"
MSAA="Maximum">
<hdx:Viewport3DX.DefaultCamera>
<hdx:PerspectiveCamera Position="0,0,15" LookDirection="0,0,-1" FieldOfView="45"/>
</hdx:Viewport3DX.DefaultCamera>
<hdx:Viewport3DX.Camera>
<hdx:PerspectiveCamera Position="0,0,15" LookDirection="0,0,-1" FieldOfView="45"/>
</hdx:Viewport3DX.Camera>
</hdx:Viewport3DX>
由于SharpDX调用GPU的内存管理,属于非托管内存,所以你需要手动管理视口的内存。
你必须在后端编写 EffectsManager="{Binding EffectsManager}",这个属性是负责接管WPF和GPU渲染管线的桥梁。如果没有写这个,你的视口将一直会是黑屏,就像这样

EffectMagaer就是初始化视口的对象,你必须手动管理他的生命周期,防止进程结束后内存残留。
using HelixToolkit;
using HelixToolkit.Geometry;
using HelixToolkit.Maths;
using HelixToolkit.SharpDX;
using HelixToolkit.Wpf.SharpDX;
using SharpDX;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Windows;
using System.Windows.Media.Media3D;
namespace SharpDX渲染框架使用示例
{
public partial class MainWindow : Window
{
public DefaultEffectsManager EffectsManager { get; } = new DefaultEffectsManager();
public MainWindow()
{
InitializeComponent();
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
MyViewport?.Dispose();
}
}
}
(2)渲染模式属性设置
写上这个,你的视口就能正常工作了(你也可以使用MVVM进行绑定,不过你需要手动管理它)
还有,你必须设置这四个负责渲染模式的属性:
OITRenderMode="DepthPeeling" <!-- 设置OIT模式,解决透明部分遮挡问题-->
EnableDeferredRendering="True" <!--开启延迟渲染,提高性能-->
EnableSwapChainRendering="True" <!--开启后,控件放在顶层,不会被其他控件遮盖,性能高;关闭后,可以被其他控件遮盖,性能低-->
MSAA="Maximum" <!--抗锯齿-->
其余属性都是视口的辅助信息显示属性,根据自己需要设置。
(3)关于相机
你可能会发现,我设置了两个相机
<hdx:Viewport3DX.DefaultCamera>
<hdx:PerspectiveCamera Position="0,0,15" LookDirection="0,0,-1" FieldOfView="45"/>
</hdx:Viewport3DX.DefaultCamera>
<hdx:Viewport3DX.Camera>
<hdx:PerspectiveCamera Position="0,0,15" LookDirection="0,0,-1" FieldOfView="45"/>
</hdx:Viewport3DX.Camera>
</hdx:Viewport3DX.DefaultCamera>
这两者的区别在于:Camera是当前实际用于渲染的相机;而DefaultCamera是一个备份,用于在用户进行“重置视图”(Zoom Extents)等操作时,将相机恢复到你预设的初始位置。
在后端代码设置相机参数的时候,通常使用这种方法设置:
public void ReSetCameraLookDir(double ms = 1000.0)
{
if (MyViewport.Camera is HelixToolkit.Wpf.SharpDX.PerspectiveCamera camera)
{
camera.ChangeDirection(new Vector3D(0,0,-1), new Vector3D(0,1,0), ms);
}
}
这是一个重置镜头的函数。
(4)模型生成算法流程
1. 使用MeshBuilder类,创建模型几何体,并生成面和法线,这里使用Cube立方体,只在Cube的朝前方的面设置法线。
2. 使用MeshBuilder的对象生成MeshGeometry3D几何体
3. 设置几何体的位置参数(根据图片大小和设置的模型坐标值来设置位置)
4. 生成纹理TextureModel,纹理是材质的一部分,就像之前说的,hlsl工作原理,你需要采样器采集纹理,然后进行渲染。
5. 生成材质,需要用到之前生成的纹理。SharpDX封装了更多材质,这里使用PhongMaterial材质,对于它的参数,我简要说明:
var material = new PhongMaterial { EmissiveMap = textureModel, //自发光的纹理 EmissiveColor = new Color4(1, 1, 1, (float)opacity), //自发光的光线参与模型ARGB通道渲染的权值 DiffuseMap = textureModel, //漫反射的纹理 DiffuseColor = new Color4(0, 0, 0, (float)opacity), // 漫反射光线对模型渲染ARGB通道的权值 AmbientColor = new Color4(0, 0, 0, 1), SpecularColor = new Color4(0, 0, 0, 1) }; 对于*Map,就是某种光打在模型体上的纹理,比如,你有一个直射光线,直射光线照在你的模型上,反射出来的纹理是什么。比方说,太阳光打在叶子上,叶子接受光,通过漫反射,返回了绿色和叶脉的纹理。 对于*Color,就是不同种光线打在模型上,模型对该种光线的反射程度,比方说,亮着的LED灯,明显是自发光要比外界光照射在他上面多得多,所以你看到的就是它自发光的纹理和颜色偏多,漫反射渲染出来的纹理权重就很少了。6. 创建模型MeshGeometryModel3D,需要用到之前的几何体和材质,
这里最关键!一定要设置IsTransparent = true ;这是解决的核心。
7. 可选部分,设置模型的渲染变换属性,如果你有需要控制模型变换的需求,记得加上。
private MeshGeometryModel3D CreateImageModel(string path, double width, double height,
double posX, double posY, double posZ,
double rotX, double rotY, double rotZ,
double opacity)
{
// 1. 使用 AddCube(BoxFaces.Front) 创建前面
var meshBuilder = new MeshBuilder();
meshBuilder.AddCube(BoxFaces.Front);
var geometry = meshBuilder.ToMeshGeometry3D();
// 2. 重新设置顶点位置,调整为指定宽高
float halfW = (float)width / 2;
float halfH = (float)height / 2;
geometry.Positions = new Vector3Collection
{
new Vector3(-halfW, -halfH, 0),
new Vector3( halfW, -halfH, 0),
new Vector3( halfW, halfH, 0),
new Vector3(-halfW, halfH, 0)
};
// 3. 加载纹理(关键:第二个参数为 false,禁用预乘Alpha,防止透明区域变黑)
var textureModel = new TextureModel(path);
// 4. 创建材质:同时使用 EmissiveMap + DiffuseMap,并将漫反射颜色设为黑色,自发光颜色设为白色,
// 这样材质不受外部光照影响,直接显示纹理原色,并且透明度正确。
var material = new PhongMaterial
{
EmissiveMap = textureModel,
EmissiveColor = new Color4(1, 1, 1, (float)opacity),
DiffuseMap = textureModel,
DiffuseColor = new Color4(0, 0, 0, (float)opacity), // 漫反射贡献为0
AmbientColor = new Color4(0, 0, 0, 1),
SpecularColor = new Color4(0, 0, 0, 1)
};
// 5. 创建模型
var model = new MeshGeometryModel3D
{
Geometry = geometry,
Material = material,
IsTransparent = true // 告诉渲染器这是透明物体
};
// 6. 设置变换(位置 + 旋转)
var transformGroup = new Transform3DGroup();
transformGroup.Children.Add(new TranslateTransform3D(posX, posY, posZ));
if (rotZ != 0)
transformGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 0, 1), rotZ)));
if (rotY != 0) transformGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(0, 1, 0), rotY)));
if (rotX != 0)
transformGroup.Children.Add(new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(1, 0, 0), rotX)));
model.Transform = transformGroup;
return model;
}
5. 项目演示

可以看到,前者没有把后面的图片遮挡,问题得以解决。
后记
SharpDX内置了很多快捷键,比如按下W,S,A,D,Q,Z,可以移动相机位置。右键鼠标移动可以旋转视角。按住Alt并右键鼠标移动可以调整视野。上下左右键也可以转动视野。
你可以自己探索更多功能。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)