地形生成器和 3D WPF 表示






4.87/5 (34投票s)
通过 WPF Viewport3D 进行的简单地形生成器和表示。
引言
本文面向所有希望接触 WPF 3D 可视化的人。我们将不再仅仅显示经典的简单正方形/三角形,而是将在 3D 环境中表示一个简单的 2D 地图。
背景
有关 WPF 中 3D 图形的基本信息,请参阅这个非常有用的简单解释。
对于地图生成,我将使用这里解释得非常清楚的算法。
Using the Code
我的示例由两个文件组成
terrainGenerator.cs
其中包含用于创建高度图
的地图生成器。
在计算机图形学中,高度图
或高度场
是一种用于存储值(例如表面高程数据)以在 3D 计算机图形中显示的栅格图像。有关更多信息,请查看此链接。
该代码是这里代码的部分移植。
引用算法
这是想法:取一个平面正方形。将其分成四个子正方形,并通过随机偏移将它们的中心点向上或向下移动。将每个子正方形分成更多子正方形并重复,每次减小随机偏移的范围,以便最初的选择最重要,而后续的选择提供更小的细节。
这就是中点位移算法。我们的钻石-正方形算法基于相似的原理,但生成更自然的结果。它不是仅仅划分为子正方形,而是在划分为子正方形和划分为子钻石之间交替。
如果您有时间,请访问该网站,该算法解释得非常清楚。它还将进一步解释带有伪光照效果的 2D 渲染。
在这里,我将给您一个简短的解释
- 我们手动设置地形高度图的四个起始角(参见“图 1”)。
- 我们通过平均角点加上一个随机值来找到正方形中心的高度图值(参见“图 2”)。
- 我们追踪一个菱形,并通过平均其菱形邻居来找到角的等高线图值(参见“图 3”)。
- 现在我们可以将生成的图像分成子正方形,如果大小大于 1 像素,我们就回到第 2 点。
MainWindow.cs
这包含主控件以及 3D 表示部分。当用户单击生成按钮(_GenerateTerrainButtonClick
)时
- 地形
高度图
的生成... //generate terrain TerrainGenerator tg = new TerrainGenerator(detailValue); tg.Generate(roughnessValue); ...
细节值表示地图的细节级别,细节越多将生成更大的地图。默认值设置为
9
(这将生成 513x513 的地图)。Map
是一个二维float
数组。数组的每个维度都按如下方式计算... _Size = (int)(Math.Pow(2, detail) + 1); _Map = new float[_Size,_Size]; ...
roughnessValue
决定地形是平滑(值接近零)还是多山(值接近一)。默认值设置为0.3
。 - 计算
高度图
的最小值和最大值计算
高度图
的最小值和最大值,以便正确居中显示地图。 - 3D 可视化
在我的代码中,XAML 文件包含
<Viewport3D Name="_MyViewport3D"> <Viewport3D.Camera> <PerspectiveCamera x:Name = "_MainPerspectiveCamera" Position = "0 0 2048" LookDirection = "0 0 -1" /> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup x:Name="_MyModel3DGroup"> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D>
Viewport3D是一个组件,它在
Viewport3D
元素的 2D 布局边界内渲染所包含的 3D 内容。它将包含我们表示 3D 场景所需的所有元素。-
就像电影中的摄像机一样。一个 3D 场景,就像在现实世界中一样,根据视角的不同而看起来不同。Camera类允许您通过设置正确的
Position
和LookDirection
变量来为 3D 场景指定此视角。
相机有不同的类型引用ProjectionCamera 允许您指定不同的投影及其属性,以改变观察者查看 3D 模型的方式。PerspectiveCamera 指定一个使场景缩短的投影。换句话说,PerspectiveCamera 提供消失点透视。您可以指定相机在场景坐标空间中的位置、相机的方向和视野,以及定义场景中“向上”方向的矢量。
https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview
-
用于照亮场景的光线。Light在 3D 图形中做着与现实世界中光线相同的事情:它们使表面可见。
光线有不同的类型引用- 环境光 (AmbientLight):提供环境光照,均匀照亮所有物体,无论其位置或方向如何。
- 定向光 (DirectionalLight):像远处的光源一样发光。定向光具有指定为 Vector3D 的 Direction,但没有指定位置。
- 点光源:像附近的灯源一样照亮。点光源有位置并从该位置发光。场景中的物体根据它们相对于灯光的位置和距离来照亮。PointLightBase 公开一个 Range 属性,该属性决定了模型不会被灯光照亮的距离。PointLight 还公开了衰减属性,这些属性决定了灯光强度随距离的减弱方式。您可以为灯光的衰减指定恒定、线性或二次插值。
- 聚光灯 (SpotLight):继承自 PointLight。聚光灯像 PointLight 一样发光,具有位置和方向。它们在由 InnerConeAngle 和 OuterConeAngle 属性(以度为单位指定)设置的锥形区域内投射光线。
https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview
在我的代码中,我在代码中添加了光线,因为我想将其放置在与地图大小相关的特定位置。
... PointLight pointLight = new PointLight (Colors.White, new Point3D(tg.Size / 2, tg.Size / 2, tg.Size * 3 / 5)); ...
Light
可以通过将它们添加到Viewport3D
来全局应用(如在现实世界中),也可以应用于特定对象/对象组以获得一些特殊效果。 -
要表示的 3D 对象。
基本上,任何表面结构都可以表示为一堆三角形。三角形是最原子和最原始的几何图形。
目前,WPF 支持使用GeometryModel3D的 3D 几何图形。
引用要构建模型,首先要构建一个图元或网格。3D 图元是构成单个 3D 实体的顶点集合。大多数 3D 系统都提供基于最简单闭合图形建模的图元:由三个顶点定义的三角形。由于三角形的三个点共面,您可以继续添加三角形以建模更复杂的形状,称为网格。
WPF 3D 系统目前提供了MeshGeometry3D类,它允许您指定任何几何图形;它目前不支持预定义的 3D 图元,如球体和立方体。通过将三角形顶点列表指定为其Positions属性来开始创建MeshGeometry3D。每个顶点都指定为Point3D。(在可扩展应用程序标记语言 (XAML) 中,此属性指定为按三组排列的数字列表,表示每个顶点的坐标。)根据其几何图形,您的网格可能由许多三角形组成,其中一些共享相同的角(顶点)。为了正确绘制网格,WPF 需要关于哪些顶点由哪些三角形共享的信息。您通过使用TriangleIndices属性指定三角形索引列表来提供此信息。此列表指定了Positions列表中指定的点将确定三角形的顺序。
https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview
对于我们世界中的每个对象,我们还可以定义该对象由何种
Material
构成。光线将根据材料规格与材料属性相互作用。引用为了定义模型表面的特征,WPF 使用Material抽象类。Material 的具体子类决定了模型表面的一些外观特征,并且每个子类还提供一个 Brush 属性,您可以向其传递 SolidColorBrush、TileBrush 或 VisualBrush。
-
漫反射材质 (DiffuseMaterial) 指定画笔将应用于模型,就像该模型被漫反射照明一样。使用 DiffuseMaterial 最类似于直接在 2D 模型上使用画笔;模型表面不会像闪亮那样反射光线。
-
镜面材质 (SpecularMaterial) 指定画笔将应用于模型,就像模型的表面坚硬或闪亮,能够反射高光。您可以通过为SpecularPower属性指定值来设置纹理暗示这种反射品质或“光泽”的程度。
-
自发光材质 (EmissiveMaterial) 允许您指定纹理将应用于模型,就像模型发出的光线与画笔的颜色相等一样。这并不会使模型成为光源;然而,它在阴影中的表现将不同于使用 DiffuseMaterial 或 SpecularMaterial 纹理的情况。
https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/graphics-multimedia/3-d-graphics-overview
在这一点小小的解释之后,我可以回到代码的解释,为了生成地形,我执行了 3 次传递
-
设计地形。
对于地形,我将使用带有统一LimeGreen
颜色的漫反射材质。
我将遍历我生成的地图,创建一个Point3d集合,其中我将X
和Y
设置为地图坐标,而对于Z
坐标,我将使用相对于高度图
最小值和最大值的高度图
值。((MeshGeometry3D)myTerrainGeometryModel.Geometry).Positions = point3DCollection;
所有这些点将通过使用三角形连接。
要执行此操作,我们必须指出要使用哪个 3D 点来生成三角形。我们可以通过创建索引集合来做到这一点。
((MeshGeometry3D)myTerrainGeometryModel.Geometry).TriangleIndices = triangleIndices;
此集合中的每个条目都是
Position
列表中的一个索引。
此列表中的每三个索引代表一个三角形。对于给定 3D 网格中的一个三角形,指定三角形顶点位置的顺序决定了三角形面是正面还是背面。WPF 3D 实现使用逆时针缠绕顺序;也就是说,从网格正面看,确定正面网格三角形位置的点应按逆时针顺序指定。/** * <summary> * Method that create the 3d terrain on a Viewport3D control * </summary> * * <param name="terrainMap">terrain to show</param> * <param name="terrainSize">terrain size</param> * <param name="minHeightValue">minimum terraing height</param> * <param name="maxHeightValue">maximum terraing height</param> */ private void _DrawTerrain(float[,] terrainMap, int terrainSize, float minHeightValue, float maxHeightValue) { float halfSize = terrainSize / 2; float halfheight = (maxHeightValue - minHeightValue) / 2; // creation of the terrain GeometryModel3D myTerrainGeometryModel = new GeometryModel3D (new MeshGeometry3D(), new DiffuseMaterial(new SolidColorBrush(Colors.GreenYellow))); Point3DCollection point3DCollection = new Point3DCollection(); Int32Collection triangleIndices = new Int32Collection(); //adding point for (var y = posY; y < maxPosY; y++) { for (var x = posX; x < maxPosX; x++) { point3DCollection.Add(new Point3D(x - halfSize, y - halfSize, terrainMap[x, y] - halfheight)); } } ((MeshGeometry3D)myTerrainGeometryModel.Geometry).Positions = point3DCollection; //defining triangles int ind1 = 0; int ind2 = 0; int xLenght = maxPosX ; for (var y = posY; y < maxPosY - 1; y++) { for (var x = posX; x < maxPosX - 1; x++) { ind1 = x + y * (xLenght); ind2 = ind1 + (xLenght); //first triangle triangleIndices.Add(ind1); triangleIndices.Add(ind2 + 1); triangleIndices.Add(ind2); //second triangle triangleIndices.Add(ind1); triangleIndices.Add(ind1 + 1); triangleIndices.Add(ind2 + 1); } } ((MeshGeometry3D)myTerrainGeometryModel.Geometry).TriangleIndices = triangleIndices; _MyModel3DGroup.Children.Add(myTerrainGeometryModel); }
-
在设计完地形后,我创建了一些图层,为我的世界添加“水效果”。
为了获得一个简单而有效的水效果,我没有使用“简单”的
漫反射材质
,而是使用了一个自发光材质,并带有均匀的蓝色
,不透明度为0.2
。
我本可以使用一个单一的正方形在特定高度来获得不错的效果,但我更喜欢使用 10 层来赋予水深度的感觉。/** * <summary> * Method that create a water effect for the terrain * </summary> * * <param name="terrainMap">terrain to show</param> * <param name="terrainSize">terrain size</param> * <param name="minHeightValue">minimum terraing height</param> * <param name="maxHeightValue">maximum terraing height</param> * <param name="waterHeightValue">water height value</param> */ private void _DrawWater(float[,] terrainMap, int terrainSize, float minHeightValue, float maxHeightValue, float waterHeightValue) { float halfSize = terrainSize / 2; float halfheight = (maxHeightValue - minHeightValue) / 2; // creation of the water layers // I'm going to use a series of emissive layer for water SolidColorBrush waterSolidColorBrush = new SolidColorBrush(Colors.Blue); waterSolidColorBrush.Opacity = 0.2; GeometryModel3D myWaterGeometryModel = new GeometryModel3D(new MeshGeometry3D(), new EmissiveMaterial(waterSolidColorBrush)); Point3DCollection waterPoint3DCollection = new Point3DCollection(); Int32Collection triangleIndices = new Int32Collection(); int triangleCounter = 0; float dfMul = 5; for (int i = 0; i < 10; i++) { triangleCounter = waterPoint3DCollection.Count; waterPoint3DCollection.Add(new Point3D(-halfSize, -halfSize, waterHeightValue - i * dfMul - halfheight)); waterPoint3DCollection.Add(new Point3D(+halfSize, +halfSize, waterHeightValue - i * dfMul - halfheight)); waterPoint3DCollection.Add(new Point3D(-halfSize, +halfSize, waterHeightValue - i * dfMul - halfheight)); waterPoint3DCollection.Add(new Point3D(+halfSize, -halfSize, waterHeightValue - i * dfMul - halfheight)); triangleIndices.Add(triangleCounter); triangleIndices.Add(triangleCounter + 1); triangleIndices.Add(triangleCounter + 2); triangleIndices.Add(triangleCounter); triangleIndices.Add(triangleCounter + 3); triangleIndices.Add(triangleCounter + 1); } ((MeshGeometry3D)myWaterGeometryModel.Geometry).Positions = waterPoint3DCollection; ((MeshGeometry3D)myWaterGeometryModel.Geometry).TriangleIndices = triangleIndices; _MyModel3DGroup.Children.Add(myWaterGeometryModel); }
-
现在我的世界已经相当完整了,但我必须构建一个包含框,以便在旋转对象时隐藏对象的一些部分。
盒子由一堵简单的黑墙组成。
-
-
-
鼠标交互的 3D 导航
对于 3D 导航,我使用了此处的代码。
关注点
当我接触 3D 时,我发现了一个非常简单直观的教程,它解释了非常基础的知识,但并没有让我投入其中。我希望这个教程能让大家更有趣地理解和使用。
这是我第一次接触 3D,因此如果您有任何修改建议,请随时与我联系。
历史
- 版本 1.0.0 - 2017 年 7 月 - 首次发布
- 版本 1.0.1 - 2017 年 7 月 - 将地图生成的细节限制为 12,3D 地形生成现在被划分为最大尺寸为 4096*4096 的单元格
- 版本 1.0.2 - 2017 年 8 月 - 优化了点定义,现在我不再重复已插入的点
- 版本 1.0.3 - 2017 年 9 月 - 优化了单元格划分,现在某些显卡可以将细节限制推到 13