跳转到内容

使用 XNA 创建游戏/3D 开发/地形建模

来自 Wikibooks,开放世界中的开放书籍

地形建模

[编辑 | 编辑源代码]
高度图(来源:维基百科)

我们如何将基于 XNA 框架的地形实现并建模到我们的游戏中?本维基条目将专门解决这个问题。例如,将展示如何使用高度图创建地形。此外,我们将创建纹理,将其拖放到我们的地形上,并编写大量源代码。最后,将提供一些关于地形建模相关主题的技巧。

高度图(维基百科:高度图)只不过是灰度图。也就是说,一个 2D 纹理,它指出我们地形的高度和深度。灰度图的每个像素都在 0 到 255 之间,表示我们的海拔高度。要创建这样的地图,请使用 Terragen 之类的程序。

Terragen 是一个用于快速创建逼真地形图像的程序。但是,它也是创建高度图的完美工具。Terragen 有两个版本(日期:2011 年 06 月 05 日),一个需要付费的版本 - Terragen 2 和一个免费版本 Terragen Classic。对于我们的需求,免费版本已经足够了。






创建高度图

[编辑 | 编辑源代码]

介绍就到这里 - 让我们开始吧。下载并安装 Terragen Classic 后,我们可以看到以下菜单

Terragen 菜单。


在左侧,我们可以看到 Terragen 提供的按钮。第一步是单击“地形”,一个新窗口将打开。在这里,我们单击“大小”来调整高度图的大小 - 257x257 或 513x513。提示:如果您已经实现了天空盒,请使用天空盒图像的大小。接下来,我们单击“视图/雕刻”来建模我们的高度图。您将看到一张带有白色箭头的黑色图片 - 这是您的相机视角。您可以通过将箭头移动到所需位置来根据需要调整视角。要开始绘制地形,您需要单击位于窗口左上角的“基本雕刻工具”(1)。现在您可以开始绘制地形了。结果应该类似于此

地形视图/雕刻窗口。


如果您对结果不满意,您始终可以单击地形窗口中的“修改”并调整某些设置,例如山脉的最大高度。另一个有用的功能是“清除/平坦化”,它可以重置您的高度图,这样您就可以重新开始。完成绘制高度图后,单击“3D 预览”按钮。它应该看起来像这样(取决于您所绘制的内容)

高度图的 3D 预览窗口。


要保存您的高度图,请单击地形菜单中的“导出”,然后选择“原始 8 位”作为导出方法(1)。单击“选择文件并保存……”,命名您的高度图并将其保存到您的硬盘驱动器。

地形导出窗口。


我们几乎完成了高度图,它现在以 .raw 格式。最后,我们需要使用 Photoshop 之类的程序或“XnView”(www.xnview.de) 等免费工具将这种格式转换为其他格式。将您的 .raw 格式更改为 .jpg、.bmp 或 .png,因为 XNA 的“默认内容管道”可以将这些格式处理为“Texture2D”。


创建纹理

[编辑 | 编辑源代码]

没有纹理,我们的地形会是什么样的?因此,让我们使用 Terragen 来创建一个纹理。为此,请在您的 Terragen 菜单中打开“渲染控制”。

首先要做的是使用“图像大小”(1)调整大小,具体取决于您创建高度图的大小(512x512 或 256x256)。在渲染控制窗口的右下角,定位您的相机,以便您实际看到地板(2)。要直接面对地板,请将俯仰(3)的值设置为 -90。这会让您直接看着地板。此外,将“细节”滑块(4)设置为最大值,以便在渲染时获得最高质量。单击“渲染预览”(5)以预览您的纹理。或者,您可以再次打开“3D 预览”,但您的纹理不会显示渲染。

渲染控制窗口。


纹理上的任何黑点可能是投射到地形上的阴影。单击 Terragen 菜单中的“照明条件”按钮,然后取消选中“地形投射阴影”和“云投射阴影”(1)以使它们消失。

照明条件窗口。


现在您已经完成了,可以单击“渲染图像”(6)在您的“渲染控制”中。Terragen 现在将渲染您的纹理,它应该看起来像这样

渲染的纹理


您也可以更改纹理的颜色。为此,请单击 Terragen 菜单中的“地形”按钮。选择“表面地图”(1),然后单击“编辑”(2)。“表面层”窗口将打开。现在单击“颜色...”(3)选择您的颜色。当您对纹理满意后,将其保存到您的硬盘驱动器。

更改纹理颜色。


尝试不同的设置,渲染它并检查更改。如果您选择白色作为颜色,您的纹理应该看起来像这样

具有不同颜色的纹理。


现在我们完成了基础部分,最终实现了我们的第一个目标 - 我们自己的高度图和纹理


在 XNA 中的实现

[编辑 | 编辑源代码]

从现在开始,我们将开始将高度图和纹理实现到 XNA 代码中。但要实际看到一些东西,我们需要从编程一个相机开始。


创建相机类

[编辑 | 编辑源代码]

我们在 Visual Studio 2008 中创建一个新项目,并添加一个名为“Camera”的新类。

我们首先分配一些类变量。一个用于相机视图的矩阵 viewMatrix 和一个用于投影的 projectionMatrix。projectionMatrix 将 3D 相机视图转换为 2D 图像。为了以后定位地形,我们将需要另一个矩阵 terrainMatrix。此外,如果我们可以在地形上移动或旋转相机,那就太好了。因此,我们为相机的位置、对齐、移动和旋转声明 Vector3 变量。

        // matrix for camera view and projection
        Matrix viewMatrix;
        Matrix projectionMatrix;

        // world matrix for our landscape
        public Matrix terrainMatrix;

        // actual camera position, direction, movement, rotation
        Vector3 position;
        Vector3 direction;
        Vector3 movement;
        Vector3 rotation;


相机构造函数获取参数来初始化所有这些变量。

 
        public Camera(Vector3 position, Vector3 direction, Vector3 movement, Vector3 landscapePosition)
        {
            this.position = position;
            this.direction = direction;
            this.movement = movement;
            rotation = movement*0.02f;
            //camera position, view of camera, see what is over camera
            viewMatrix = Matrix.CreateLookAt(position, direction, Vector3.Up);
            //width and height of camera near plane, range of camera far plane (1-1000)
            projectionMatrix = Matrix.CreatePerspective(1.2f, 0.9f, 1.0f, 1000.0f);
            // positioning our landscape in camera start position
            terrainMatrix = Matrix.CreateTranslation(landscapePosition);
        }


现在,如果您想知道 CreateLookAt()、CreatePerspective()、CreateTranslation() 究竟做了什么,请查看 XNA 框架的类库 -> XNA 框架类库参考。所有方法都在其中清楚地描述。请记住 XNA 框架类库,以检查您不清楚的所有方法,因为源代码中使用的并非所有方法都将详细解释。

为了至少练习一次,我们使用 CreateTranslation() 方法。转到 Matrix.CreatePerspective 方法 (Single, Single, Single, Single),您将找到对方法使用的所有参数以及它们的返回值的详细描述。

CreatePerspective() 方法的参数和返回值


回到我们的相机类。下一步是创建一个 Update() 方法,它将获取一个数字作为参数。在此方法中,我们定义了相机的移动和旋转,并在最后计算了新的相机位置。我们这样做是因为,当我们稍后在 Game1.cs 中创建一个相机时,我们可以使用键盘输入来移动相机。每个键盘输入都会发送一个数字,该数字将由相机的 Update() 方法处理。

 
        public void Update(int number)
        {
            Vector3 tempMovement = Vector3.Zero;
            Vector3 tempRotation = Vector3.Zero;
            //left
            if (number == 1)
            {
                tempMovement.X = +movement.X;
            }
            //right
            if (number == 2)
            {
                tempMovement.X = -movement.X;
            }
            //up
            if (number == 3)
            {
                tempMovement.Y = -movement.Y;
            }
            //down
            if (number == 4)
            {
                tempMovement.Y = +movement.Y;
            }
            //backward (zoomOut)
            if (number == 5)
            {
                tempMovement.Z = -movement.Z;
            }
            //forward (zoomIn)
            if (number == 6)
            {
                tempMovement.Z = +movement.Z;
            }
            //left rotation
            if (number == 7)
            {
                tempRotation.Y = -rotation.Y;
            }
            //right rotation
            if (number == 8)
            {
                tempRotation.Y = +rotation.Y;
            }
            //forward rotation
            if (number == 9)
            {
                tempRotation.X = -rotation.X;
            }
            //backward rotation
            if (number == 10)
            {
                tempRotation.X = +rotation.X;
            }

            //move camera to new position
            viewMatrix = viewMatrix * Matrix.CreateRotationX(tempRotation.X) * Matrix.CreateRotationY(tempRotation.Y) * Matrix.CreateTranslation(tempMovement);
            //update position
            position += tempMovement;
            direction += tempRotation;
        }


最后,我们的相机获得一个 Draw() 方法。在此方法中,我们传递地形,以确保它稍后显示。

 
        public void Draw(Terrain terrain)
        {
            terrain.basicEffect.Begin();
            SetEffects(terrain.basicEffect);
            foreach (EffectPass pass in terrain.basicEffect.CurrentTechnique.Passes)
            {
                pass.Begin();
                terrain.Draw();
                pass.End();
            }
            terrain.basicEffect.End();
        }


在我们开始编写 Terrain.cs 类之前,我们需要实现 Draw() 方法使用的 SetEffects() 方法。BasicEffect 是 XNA 框架中的一个类,它提供渲染效果来显示对象。

        public void SetEffects(BasicEffect basicEffect)
        {
            basicEffect.View = viewMatrix;
            basicEffect.Projection = projectionMatrix;
            basicEffect.World = terrainMatrix;
        }


现在,我们的 Camera.cs 类已经准备好了,为了实际看到一些东西,我们现在开始编写 Terrain.cs 类。


Camera.cs 类概述

[编辑 | 编辑源代码]

完整的 Camera.cs 类应该如下所示。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace WindowsGame1
{
    class Camera
    {
        // matrix for camera view and projection
        Matrix viewMatrix;
        Matrix projectionMatrix;

        // world matrix for our landscape
        public Matrix terrainMatrix;

        // actual camera position, direction, movement, rotation
        Vector3 position;
        Vector3 direction;
        Vector3 movement;
        Vector3 rotation;
        
        public Camera(Vector3 position, Vector3 direction, Vector3 movement, Vector3 landscapePosition)
        {
            this.position = position;
            this.direction = direction;
            this.movement = movement;
            rotation = movement*0.02f;
            //camera position, view of camera, see what is over camera
            viewMatrix = Matrix.CreateLookAt(position, direction, Vector3.Up);
            //width and height of camera near plane, range of camera far plane (1-1000)
            projectionMatrix = Matrix.CreatePerspective(1.2f, 0.9f, 1.0f, 1000.0f);
            // positioning our landscape in camera start position
            terrainMatrix = Matrix.CreateTranslation(landscapePosition);
        }

        public void Update(int number)
        {
            Vector3 tempMovement = Vector3.Zero;
            Vector3 tempRotation = Vector3.Zero;
            //left
            if (number == 1)
            {
                tempMovement.X = +movement.X;
            }
            //right
            if (number == 2)
            {
                tempMovement.X = -movement.X;
            }
            //up
            if (number == 3)
            {
                tempMovement.Y = -movement.Y;
            }
            //down
            if (number == 4)
            {
                tempMovement.Y = +movement.Y;
            }
            //backward (zoomOut)
            if (number == 5)
            {
                tempMovement.Z = -movement.Z;
            }
            //forward (zoomIn)
            if (number == 6)
            {
                tempMovement.Z = +movement.Z;
            }
            //left rotation
            if (number == 7)
            {
                tempRotation.Y = -rotation.Y;
            }
            //right rotation
            if (number == 8)
            {
                tempRotation.Y = +rotation.Y;
            }
            //forward rotation
            if (number == 9)
            {
                tempRotation.X = -rotation.X;
            }
            //backward rotation
            if (number == 10)
            {
                tempRotation.X = +rotation.X;
            }

            //move camera to new position
            viewMatrix = viewMatrix * Matrix.CreateRotationX(tempRotation.X) * Matrix.CreateRotationY(tempRotation.Y) * Matrix.CreateTranslation(tempMovement);
            //update position
            position += tempMovement;
            direction += tempRotation;
        }

        public void SetEffects(BasicEffect basicEffect)
        {
            basicEffect.View = viewMatrix;
            basicEffect.Projection = projectionMatrix;
            basicEffect.World = terrainMatrix;
        }
                
        public void Draw(Terrain terrain)
        {
            terrain.basicEffect.Begin();
            SetEffects(terrain.basicEffect);
            foreach (EffectPass pass in terrain.basicEffect.CurrentTechnique.Passes)
            {
                pass.Begin();
                terrain.Draw();
                pass.End();
            }
            terrain.basicEffect.End();
        }
    }
}

创建地形类

[编辑 | 编辑源代码]

创建一个新类并将其重命名为 Terrain.cs。我们首先定义所需的类变量。我们需要 Texture2D 变量来保存我们的高度图和纹理图像,以及用于处理纹理的变量,特别是数组。

        GraphicsDevice graphicsDevice;

        // heightMap
        Texture2D heightMap;
        Texture2D heightMapTexture;
        VertexPositionTexture[] vertices;
        int width; 
        int height;

        public BasicEffect basicEffect;
        int[] indices;

        // array to read heightMap data
        float[,] heightMapData;


在我们的 Terrain.cs 的构造函数中,我们调用 GraphicsDevice 单位以便能够在我们的类中访问它。

        public Terrain(GraphicsDevice graphicsDevice)
        {
            this.graphicsDevice = graphicsDevice;
        }


现在我们创建一个方法,该方法将获取我们的纹理(这将来自 Game1.cs 类,稍后解释)并调用其他方法,以便我们更接近我们的地形。所以让我们编写缺少的方法。

        public void SetHeightMapData(Texture2D heightMap, Texture2D heightMapTexture)
        {
            this.heightMap = heightMap;
            this.heightMapTexture = heightMapTexture;
            width = heightMap.Width;
            height = heightMap.Height;
            SetHeights();
            SetVertices();
            SetIndices();
            SetEffects();
        }


我们首先实现 SetHeight() 方法,该方法将从纹理的每个像素获取灰度值,表示其实际高度,并将它们写入 heightMapData[] 数组中。完整方法

public void SetHeights()
        {
            Color[] greyValues = new Color[width * height];
            heightMap.GetData(greyValues);
            heightMapData = new float[width, height];
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    heightMapData[x, y] = greyValues[x + y * width].G / 3.1f;
                }
            }
        }


要获取每个灰度的强度,只需获取单个颜色的值,可以是红色、绿色或蓝色 - 您选择哪一个都可以。为了避免高度相差太大,您可以将颜色值除以一个值。因此,这行代码

heightMapData[x, y] = greyValues[x + y * width].G / 3.1f;


它也可以反过来。当您乘以一个值时,您将获得更高的海拔差异。

接下来的两个方法处理索引和顶点的创建。SetVertice() 使用三角形创建我们的地形区域。一个区域由两个三角形组成。三角形可以通过 3 个数字来描述,这些数字称为索引。这些三角形的索引被分配给顶点。如果您需要对此进行复习,请查看 Riemer 的 XNA 教程 -> 使用索引回收顶点.

在我们的方法中,使用了一些奇怪的数学运算来计算正确的索引。稍微试着调整一下,看看改变某些值会发生什么。

 
        public void SetIndices()
        {
            // amount of triangles
            index = new int[6 * (width - 1) * (height - 1)];
            int number = 0;
            // collect data for corners
            for (int y = 0; y < height - 1; y++)
                for (int x = 0; x < width - 1; x++)
                {
                    // create double triangles
                    index[number] = x + (y + 1) * width;      // up left
                    index[number + 1] = x + y * width + 1;        // down right
                    index[number + 2] = x + y * width;            // down left
                    index[number + 3] = x + (y + 1) * width;      // up left
                    index[number + 4] = x + (y + 1) * width + 1;  // up right
                    index[number + 5] = x + y * width + 1;        // down right
                    number += 6;
                }
        }


SetVertices() 方法计算纹理应应用于每个顶点的 2D 位置。高度和深度将使用来自 heightMapData[] 数组的数据进行分配。

public void SetVertices()
        {
            vertices = new VertexPositionTexture[width * height];
            Vector2 texturePosition;
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    texturePosition = new Vector2((float)x / 25.5f, (float)y / 25.5f);
                    vertices[x + y * width] = new VertexPositionTexture(new Vector3(x, heightMapData[x, y], -y), texturePosition);
                }
                graphicsDevice.VertexDeclaration = new VertexDeclaration(graphicsDevice, VertexPositionTexture.VertexElements);
            }
        }


现在我们在其中使用一个新的 BasicEffet 类型(维基百科:着色器)的着色器对象来实现 SetEffects() 方法。它的纹理属性被分配到我们的地形纹理,并且它的显示被激活。

 
        public void SetEffects()
        {
            basicEffect = new BasicEffect(graphicsDevice, null);
            basicEffect.Texture = heightMapTexture;
            basicEffect.TextureEnabled = true;
        }


为了实际绘制地形,我们的 terrain.cs 类获得了一个自己的 Draw() 方法。从这里,我们调用 DrawUserIndexedPrimitives() 方法(来自 XNA 的 GraphicsDevice 类),该方法非常强大,包含一个很长的参数列表。首先是将要绘制的对象类型。使用 TriangleList 时,表示三角形集合。后面是我们的包含顶点的数组。接下来的参数接受我们的顶点的起点和数量。接下来是包含我们索引的数组,最后是第一个三角形的数量和三角形的数量。

        public void Draw()
        {
           graphicsDevice.DrawUserIndexedPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
        }


最后但并非最不重要的是,我们需要调整我们的 Game1.cs,在其中我们现在调用我们的相机和地形来实现我们的目标,即查看我们的地形。


概述 Terrain.cs 类

[编辑 | 编辑源代码]

在此之前,完整 Terrain.cs 类的概述

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;


namespace WindowsGame1
{
    public class Terrain
    {
        GraphicsDevice graphicsDevice;

        // heightMap
        Texture2D heightMap;
        Texture2D heightMapTexture;
        VertexPositionTexture[] vertices;
        int width; 
        int height;

        public BasicEffect basicEffect;
        int[] indices;

        // array to read heightMap data
        float[,] heightMapData;
        


        public Terrain(GraphicsDevice graphicsDevice)
        {
            this.graphicsDevice = graphicsDevice;
        }

        public void SetHeightMapData(Texture2D heightMap, Texture2D heightMapTexture)
        {
            this.heightMap = heightMap;
            this.heightMapTexture = heightMapTexture;
            width = heightMap.Width;
            height = heightMap.Height;
            SetHeights();
            SetVertices();
            SetIndices();
            SetEffects();
        }

        public void SetHeights()
        {
            Color[] greyValues = new Color[width * height];
            heightMap.GetData(greyValues);
            heightMapData = new float[width, height];
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    heightMapData[x, y] = greyValues[x + y * width].G / 3.1f;
                }
            }
        }

        public void SetIndices()
        {
            // amount of triangles
            indices = new int[6 * (width - 1) * (height - 1)];
            int number = 0;
            // collect data for corners
            for (int y = 0; y < height - 1; y++)
                for (int x = 0; x < width - 1; x++)
                {
                    // create double triangles
                    indices[number] = x + (y + 1) * width;      // up left
                    indices[number + 1] = x + y * width + 1;        // down right
                    indices[number + 2] = x + y * width;            // down left
                    indices[number + 3] = x + (y + 1) * width;      // up left
                    indices[number + 4] = x + (y + 1) * width + 1;  // up right
                    indices[number + 5] = x + y * width + 1;        // down right
                    number += 6;
                }
        }

        public void SetVertices()
        {
            vertices = new VertexPositionTexture[width * height];
            Vector2 texturePosition;
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    texturePosition = new Vector2((float)x / 25.5f, (float)y / 25.5f);
                    vertices[x + y * width] = new VertexPositionTexture(new Vector3(x, heightMapData[x, y], -y), texturePosition);
                }
                graphicsDevice.VertexDeclaration = new VertexDeclaration(graphicsDevice, VertexPositionTexture.VertexElements);
            }
        }
        
       

        public void SetEffects()
        {
            basicEffect = new BasicEffect(graphicsDevice, null);
            basicEffect.Texture = heightMapTexture;
            basicEffect.TextureEnabled = true;
        }

        public void Draw()
        {
           graphicsDevice.DrawUserIndexedPrimitives<VertexPositionTexture>(PrimitiveType.TriangleList, vertices, 0, vertices.Length, indices, 0, indices.Length / 3);
        }
        
    }
}


调整 Game1.cs 类

[编辑 | 编辑源代码]

在我们开始之前,我们将我们的高度图以及我们的纹理图像导入 VisualStudio2008。右键单击项目资源管理器中的内容。在弹出的菜单中选择“添加” -> “现有元素...”。选择您的图像并导入它们。现在您应该在“内容”下看到您的高度图和纹理图像。现在创建您的相机和地形作为类变量。

        //-------------CAMERA------------------
        Camera camera;

        //-------------TERRAIN-----------------
        Terrain landscape;


为了让 VisualStudio2008 知道在哪里找到您的图像,在构造函数中添加以下行

Content.RootDirectory = "Content";


接下来使用 Initialize() 方法初始化您的相机和地形。

            // initialize camera start position
            camera = new Camera(new Vector3(-100, 0, 0), Vector3.Zero, new Vector3(2, 2, 2), new Vector3(0, -100, 256));
                   
            // initialize terrain
            landscape = new Terrain(GraphicsDevice);


如果您后来什么也看不到,您可能需要调整传递到相机类的 Vector3 向量。

LoadContent() 方法中的以下行用于将高度图和纹理图像加载到您的地形类中

            //load heightMap and heightMapTexture to create landscape
           landscape.SetHeightMapData(Content.Load<Texture2D>("heightMap"), Content.Load<Texture2D>("heightMapTexture"));


因为我们将相机类编程为向前看,并且想要将相机移动到我们的地形上,所以我们只需要在 Update() 方法中定义移动键即可。

// move camera position with keyboard
            KeyboardState key = Keyboard.GetState();
            if (key.IsKeyDown(Keys.A))
            {
                camera.Update(1);
            }
            if (key.IsKeyDown(Keys.D))
            {
                camera.Update(2);
            }
            if (key.IsKeyDown(Keys.W)) 
            { 
                camera.Update(3); 
            }
            if (key.IsKeyDown(Keys.S))
            {
                camera.Update(4);
            }
            if (key.IsKeyDown(Keys.F))
            {
                camera.Update(5);
            }
            if (key.IsKeyDown(Keys.R))
            {
                camera.Update(6);
            }
            if (key.IsKeyDown(Keys.Q))
            {
                camera.Update(7);
            }
            if (key.IsKeyDown(Keys.E))
            {
                camera.Update(8);
            }
            if (key.IsKeyDown(Keys.G))
            {
                camera.Update(9);
            }
            if (key.IsKeyDown(Keys.T))
            {
                camera.Update(10);
            }


最后但并非最不重要的是,我们需要告诉相机的 Draw() 方法绘制我们的地形。

            // to get landscape viewable
            camera.Draw(landscape);


概述 Game1.cs 类

[编辑 | 编辑源代码]
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace WindowsGame1
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        //-------------CAMERA------------------
        Camera camera;

        //-------------TERRAIN-----------------
        Terrain landscape;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

 
        protected override void Initialize()
        {
            // initialize camera start position
            camera = new Camera(new Vector3(-100, 0, 0), Vector3.Zero, new Vector3(2, 2, 2), new Vector3(0, -100, 256));
            
            // initialize terrain
            landscape = new Terrain(GraphicsDevice);

            base.Initialize();
        }

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            //load heightMap and heightMapTexture to create landscape
           landscape.SetHeightMapData(Content.Load<Texture2D>("heightMap"), Content.Load<Texture2D>("heightMapTexture"));
        }
   
        protected override void Update(GameTime gameTime)
        {
            // move camera position with keyboard
            KeyboardState key = Keyboard.GetState();
            if (key.IsKeyDown(Keys.A))
            {
                camera.Update(1);
            }
            if (key.IsKeyDown(Keys.D))
            {
                camera.Update(2);
            }
            if (key.IsKeyDown(Keys.W)) 
            { 
                camera.Update(3); 
            }
            if (key.IsKeyDown(Keys.S))
            {
                camera.Update(4);
            }
            if (key.IsKeyDown(Keys.F))
            {
                camera.Update(5);
            }
            if (key.IsKeyDown(Keys.R))
            {
                camera.Update(6);
            }
            if (key.IsKeyDown(Keys.Q))
            {
                camera.Update(7);
            }
            if (key.IsKeyDown(Keys.E))
            {
                camera.Update(8);
            }
            if (key.IsKeyDown(Keys.G))
            {
                camera.Update(9);
            }
            if (key.IsKeyDown(Keys.T))
            {
                camera.Update(10);
            }
            base.Update(gameTime);
        }

        
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue); 
            // to get landscape viewable
            camera.Draw(landscape);
    
            base.Draw(gameTime);
        }
    }
}


恭喜- 我们完成了。

作为您的工作的结果,您现在应该看到您的地形,其高度图和纹理由调试器生成。此外,您可以将相机移动到您的地形上,以确认您确实拥有高度和深度。


如果您想知道您的地形作为三角形网格的样子,请转到 Terrain.cs 类的 SetEffects() 方法,并像这样修改它

 
        public void SetEffects()
        {
            basicEffect = new BasicEffect(graphicsDevice, null);
            basicEffect.Texture = heightMapTexture;
            basicEffect.TextureEnabled = false;
            graphicsDevice.RenderState.FillMode = FillMode.WireFrame;
        }



现在您可以通过简单地使用不同的高度图轻松替换整个地形。纹理也是如此。只需在 Terrain.cs 类中的 SetHeightMapData() 方法中使用新图像的新名称作为参数。

 
landscape.SetHeightMapData(Content.Load<Texture2D>("heightMap"), Content.Load<Texture2D>("heightMapTexture"));


[编辑 | 编辑源代码]

不幸的是,XNA 的基本着色器 (BasicEffect) 只能处理一种纹理。为了改进您的地形,您现在可以编写自己的 EffectShader 文件,该文件可以处理多种纹理。如果您对着色器感兴趣,请查看 使用 XNA 创建游戏/3D 开发/着色器和效果。您可以使用多纹理使您的地形更有趣。

也可以使用 3D 建模软件创建地形,并将其作为 .x 或 .fbx 文件导入。这样做将需要更多的 CPU 能力和 3D 建模软件的知识。查看 使用 XNA 创建游戏/3D 开发/导入模型.

另一个非常复杂的主题是检测在您地形表面移动的对象的碰撞。查看 使用 XNA 创建游戏/数学物理/碰撞检测。使用下图进行简要介绍。

插值


蓝色圆圈是对象(可能是您的游戏角色)。该对象必须始终请求您地形在其移动方向上的 y 位置(绿色线)。为了在您的地形海拔发生变化时获得平滑的移动,您需要对您对象向量的当前位置的 y 值与您地形向量的新的 y 值(目的地)进行插值(维基百科:插值)。图像中地形的 y 值从 15 变化到 23。

您可以在此处找到有关此主题和一些代码的更多信息

碰撞系列 4:与高度图碰撞

碰撞系列 5:带法线的高度图碰撞



  • Microsoft XNA Game Studio 3.0,Chad Carter
  • Microsoft XNA Game Studio 创建者指南第二版,S. Cawood 和 P. McGee
  • 使用 XNA 框架进行游戏编程,Hans-Georg Schumann


RayIncarnation

华夏公益教科书