65.9K
CodeProject 正在变化。 阅读更多。
Home

地形生成器和 3D WPF 表示

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (34投票s)

2017 年 7 月 24 日

CPOL

9分钟阅读

viewsIcon

39641

downloadIcon

1403

通过 WPF Viewport3D 进行的简单地形生成器和表示。

引言

本文面向所有希望接触 WPF 3D 可视化的人。我们将不再仅仅显示经典的简单正方形/三角形,而是将在 3D 环境中表示一个简单的 2D 地图。

背景

有关 WPF 中 3D 图形的基本信息,请参阅这个非常有用的简单解释

对于地图生成,我将使用这里解释得非常清楚的算法。

Using the Code

我的示例由两个文件组成

terrainGenerator.cs

其中包含用于创建高度图的地图生成器。
在计算机图形学中,高度图高度场是一种用于存储值(例如表面高程数据)以在 3D 计算机图形中显示的栅格图像。有关更多信息,请查看此链接

该代码是这里代码的部分移植。

引用

算法

这是想法:取一个平面正方形。将其分成四个子正方形,并通过随机偏移将它们的中心点向上或向下移动。将每个子正方形分成更多子正方形并重复,每次减小随机偏移的范围,以便最初的选择最重要,而后续的选择提供更小的细节。

这就是中点位移算法。我们的钻石-正方形算法基于相似的原理,但生成更自然的结果。它不是仅仅划分为子正方形,而是在划分为子正方形和划分为子钻石之间交替。

如果您有时间,请访问该网站,该算法解释得非常清楚。它还将进一步解释带有伪光照效果的 2D 渲染。
在这里,我将给您一个简短的解释

  1. 我们手动设置地形高度图的四个起始角(参见“图 1”)。
  2. 我们通过平均角点加上一个随机值来找到正方形中心的高度图值(参见“图 2”)。
  3. 我们追踪一个菱形,并通过平均其菱形邻居来找到角的等高线图值(参见“图 3”)。
  4. 现在我们可以将生成的图像分成子正方形,如果大小大于 1 像素,我们就回到第 2 点。

MainWindow.cs

这包含主控件以及 3D 表示部分。当用户单击生成按钮(_GenerateTerrainButtonClick)时

  1. 地形高度图的生成
    ...
    
    //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

  2. 计算高度图的最小值和最大值

    计算高度图的最小值和最大值,以便正确居中显示地图。

  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类允许您通过设置正确的PositionLookDirection变量来为 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集合,其中我将XY设置为地图坐标,而对于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);
        }
      • 现在我的世界已经相当完整了,但我必须构建一个包含框,以便在旋转对象时隐藏对象的一些部分。

        盒子由一堵简单的黑墙组成。

  4. 鼠标交互的 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
© . All rights reserved.