3D系列4.2 添加一个第一人称相机

让我们首先添加一个可移动的相机,这样接下来就可以自由地在地形上移动。我们已经在系列2中看到一个基于四元数的相机,但对于这个程序相机动得太多了,因为相机事实上无法绕任意方向旋转。

相机的详尽知识可参见2.3 创建一个第一人称射击游戏(FPS)的相机:Quake风格的相机

通常你需要一个fps相机可以绕up向量(转弯)和side向量(上下看)旋转。所以无需存储一个旋转矩阵,只需存储绕up向量和side向量的旋转量就可以了。所以创建这些变量存储相机定义,放在代码顶部:

Vector3 cameraPosition = new Vector3(130, 30, -50);
float leftrightRot = MathHelper.PiOver2;
float updownRot = -MathHelper.Pi / 10.0f;
const float rotationSpeed = 0.3f;
const float moveSpeed = 30.0f;

最后两个变量表示相机根据鼠标或键盘输入移动的快慢,因此为常数。

首先创建UpdateViewMatrix方法,这个方法基于当前相机位置和旋转创建一个视矩阵。更多细节可见2.1 创建一个相机:Position,Target和View Frustum,要创建视矩阵,我们需要相机位置,相机的观察目标和相机的向上方向,后两个向量是由相机旋转决定的,所以首先需要基于当前旋转值构建相机的旋转矩阵:

private void UpdateViewMatrix()
 {
     Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
 
     Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
     Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
     Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget;
 
     Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);
     Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
 
     viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);
 }

第一行代码创建相机旋转矩阵,这是通过组合绕X轴的旋转(上下看)和绕Y轴的旋转(左右看)实现的。

我们可以将相机的位置加上(0,0,-1) forward向量作为相机的观察目标。我们需要将forward向量通过相机的旋转进行变换,让它变成相机的向前向量。变换up向量的方法是一样的:使用相机旋转进行变换。

有了变换过的forward向量和up向量,很容易构建视矩阵。

现在不是在LoadContent方法设置固定的视矩阵,而是在LoadContent方法中调用UpdateViewMatrix方法:

UpdateViewMatrix();

然后要让相机可以根据用户输入进行动作。我们想当按下键盘上的箭头键时可以让相机前进后退左右平移,相机旋转由鼠标控制。

我们需要创建一个方法,这个方法以自上一次调用以来经过的时间为参数,这样相机的移动速度就与电脑的运行速度无关了:

private void ProcessInput(float amount)
 {
     Vector3 moveVector = new Vector3(0, 0, 0);
     KeyboardState keyState = Keyboard.GetState();
     if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W))
         moveVector += new Vector3(0, 0, -1);
     if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S))
         moveVector += new Vector3(0, 0, 1);
     if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D))
         moveVector += new Vector3(1, 0, 0);
     if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A))
         moveVector += new Vector3(-1, 0, 0);
     if (keyState.IsKeyDown(Keys.Q))
         moveVector += new Vector3(0, 1, 0);
     if (keyState.IsKeyDown(Keys.Z))
         moveVector += new Vector3(0, -1, 0);            
     AddToCameraPosition(moveVector * amount);
 }

这个方法获取键盘状态,并由此设置moveVector变量,moveVector变量会乘以amount变量,表示自上一次调用以来经历的时间,乘积传递到AddToCameraPosition方法,如下所示:

private void AddToCameraPosition(Vector3 vectorToAdd)
 {
     Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
     Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation);
     cameraPosition += moveSpeed * rotatedVector;
     UpdateViewMatrix();
 }

这个方法再次创建相机的旋转矩阵,然后使用这个矩阵转换移动方向。这是必须的,因为你想让相机沿着自己的Forward方向前进,而不是沿着(0,0,-1)方向前进。

当然还需要在Update 方法中调用ProcessInput方法:

float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f; 
ProcessInput(timeDifference); 

运行代码,你就可以使用箭头键移动相机了。

下面看一下如何使用鼠标控制相机的旋转。我们读取鼠标状态MouseState,MouseState结构包含光标的绝对X和Y位置的信息,但没有包含上一次调用以来光标的移动量。

在每一帧的最后,我们都要将光标重新放置到屏幕中央。通过比较鼠标的当前位置和屏幕的中央位置,我们可以检查每帧鼠标移动了多少距离!

所以需要添加这个变量:

MouseState originalMouseState;

在项目的开始处,你需要在LoadContent方法中放置以下代码让鼠标位于屏幕中央并保存这个鼠标状态:

Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2); 
originalMouseState = Mouse.GetState(); 

下面的代码需要放在ProcessInput方法的顶部:

MouseState currentMouseState = Mouse.GetState();
 if (currentMouseState != originalMouseState)
 {
     float xDifference = currentMouseState.X - originalMouseState.X;
     float yDifference = currentMouseState.Y - originalMouseState.Y;
     leftrightRot -= rotationSpeed * xDifference * amount;
     updownRot -= rotationSpeed * yDifference * amount;
     Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2);
     UpdateViewMatrix();
 }

首先获取当前的MouseState,然后检查它是否和初始的鼠标位置有区别。如果有,对应的旋转值会根据鼠标的移动距离进行更新,并乘以上一帧以来经历的时间。

好了!当运行代码时,你就可以在地形上行走了。

注意:因为我们一直要将光标设置到窗口中央,就无法点击窗口右上方的X关闭窗口,但可以使用Alt+F4关闭正在运行的窗口。截图看起来和上一章没什么区别,但现在可以控制相机的移动了。

程序截图

因为HLSL代码没有变化,我只列出了XNA代码,红色为代码中相对上一章改变的部分:

using System;
 using System.Collections.Generic;
 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.Net;
 using Microsoft.Xna.Framework.Storage;
 
 namespace XNAseries4
 {
     public struct VertexPositionNormalColored
     {
         public Vector3 Position;
         public Color Color;
         public Vector3 Normal;
 
         public static int SizeInBytes = 7 * 4;
         public static VertexElement[] VertexElements = new VertexElement[]
              {
                  new VertexElement( 0, 0, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Position, 0 ),
                  new VertexElement( 0, sizeof(float) * 3, VertexElementFormat.Color, VertexElementMethod.Default, VertexElementUsage.Color, 0 ),
                  new VertexElement( 0, sizeof(float) * 4, VertexElementFormat.Vector3, VertexElementMethod.Default, VertexElementUsage.Normal, 0 ),
              };
     }
 
     public class Game1 : Microsoft.Xna.Framework.Game
     {
         GraphicsDeviceManager graphics;
         GraphicsDevice device;
 
         int terrainWidth;
         int terrainLength;
         float[,] heightData;
 
         VertexBuffer terrainVertexBuffer;
         IndexBuffer terrainIndexBuffer;
         VertexDeclaration terrainVertexDeclaration;
 
         Effect effect;
         Matrix viewMatrix;
         Matrix projectionMatrix;
 
         Vector3 cameraPosition = new Vector3(130, 30, -50);
         float leftrightRot = MathHelper.PiOver2;
         float updownRot = -MathHelper.Pi / 10.0f;
         const float rotationSpeed = 0.3f;
         const float moveSpeed = 30.0f;
         MouseState originalMouseState;
 
         public Game1()
         {
             graphics = new GraphicsDeviceManager(this);
             Content.RootDirectory = "Content";
         }
 
         protected override void Initialize()
         {
             graphics.PreferredBackBufferWidth = 500;
             graphics.PreferredBackBufferHeight = 500;
 
             graphics.ApplyChanges();
             Window.Title = "Riemer's XNA Tutorials -- Series 4";
 
             base.Initialize();
         }
 
         protected override void LoadContent()
         {
             device = GraphicsDevice;

            effect = Content.Load<Effect> ("Series4Effects");
             UpdateViewMatrix();
             projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.PiOver4, device.Viewport.AspectRatio, 0.3f, 1000.0f);
 
              Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2);
             originalMouseState = Mouse.GetState();
 
             LoadVertices();
         }
 
         private void LoadVertices()
         {

            Texture2D heightMap = Content.Load<Texture2D> ("heightmap");            LoadHeightData(heightMap);

            VertexPositionNormalColored[] terrainVertices = SetUpTerrainVertices();
            int[] terrainIndices = SetUpTerrainIndices();
            terrainVertices = CalculateNormals(terrainVertices, terrainIndices);
            CopyToTerrainBuffers(terrainVertices, terrainIndices);
            terrainVertexDeclaration = new VertexDeclaration(device, VertexPositionNormalColored.VertexElements);
        }

        private void LoadHeightData(Texture2D heightMap)
        {
            float minimumHeight = float.MaxValue;
            float maximumHeight = float.MinValue;

            terrainWidth = heightMap.Width;
            terrainLength = heightMap.Height;

            Color[] heightMapColors = new Color[terrainWidth * terrainLength];
            heightMap.GetData(heightMapColors);

            heightData = new float[terrainWidth, terrainLength];
            for (int x = 0; x < terrainWidth; x++)
                for (int y = 0; y < terrainLength; y++)
                {
                    heightData[x, y] = heightMapColors[x + y * terrainWidth].R;
                    if (heightData[x, y] < minimumHeight) minimumHeight = heightData[x, y];
                    if (heightData[x, y] > maximumHeight) maximumHeight = heightData[x, y];
                }

            for (int x = 0; x < terrainWidth; x++)
                for (int y = 0; y < terrainLength; y++)
                    heightData[x, y] = (heightData[x, y] - minimumHeight) / (maximumHeight - minimumHeight) * 30.0f;
        }

        private VertexPositionNormalColored[] SetUpTerrainVertices()
        {
            VertexPositionNormalColored[] terrainVertices = new VertexPositionNormalColored[terrainWidth * terrainLength];

            for (int x = 0; x < terrainWidth; x++)
            {
                for (int y = 0; y < terrainLength; y++)
                {
                    terrainVertices[x + y * terrainWidth].Position = new Vector3(x, heightData[x, y], -y);

                    if (heightData[x, y] < 6)
                        terrainVertices[x + y * terrainWidth].Color = Color.Blue;
                    else if (heightData[x, y] < 15)
                        terrainVertices[x + y * terrainWidth].Color = Color.Green;
                    else if (heightData[x, y] < 25)
                        terrainVertices[x + y * terrainWidth].Color = Color.Brown;
                    else
                        terrainVertices[x + y * terrainWidth].Color = Color.White;
                }
            }

            return terrainVertices;
        }

        private int[] SetUpTerrainIndices()
        {
            int[] indices = new int[(terrainWidth - 1) * (terrainLength - 1) * 6];
            int counter = 0;
            for (int y = 0; y < terrainLength - 1; y++)
            {
                for (int x = 0; x < terrainWidth - 1; x++)
                {
                    int lowerLeft = x + y * terrainWidth;
                    int lowerRight = (x + 1) + y * terrainWidth;
                    int topLeft = x + (y + 1) * terrainWidth;
                    int topRight = (x + 1) + (y + 1) * terrainWidth;

                    indices[counter++] = topLeft;
                    indices[counter++] = lowerRight;
                    indices[counter++] = lowerLeft;

                    indices[counter++] = topLeft;
                    indices[counter++] = topRight;
                    indices[counter++] = lowerRight;
                }
            }

            return indices;
        }

        private VertexPositionNormalColored[] CalculateNormals(VertexPositionNormalColored[] vertices, int[] indices)
        {
            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal = new Vector3(0, 0, 0);

            for (int i = 0; i < indices.Length / 3; i++)
            {
                int index1 = indices[i * 3];
                int index2 = indices[i * 3 + 1];
                int index3 = indices[i * 3 + 2];

                Vector3 side1 = vertices[index1].Position - vertices[index3].Position;
                Vector3 side2 = vertices[index1].Position - vertices[index2].Position;
                Vector3 normal = Vector3.Cross(side1, side2);

                vertices[index1].Normal += normal;
                vertices[index2].Normal += normal;
                vertices[index3].Normal += normal;
            }

            for (int i = 0; i < vertices.Length; i++)
                vertices[i].Normal.Normalize();

            return vertices;
        }

        private void CopyToTerrainBuffers(VertexPositionNormalColored[] vertices, int[] indices)
        {
            terrainVertexBuffer = new VertexBuffer(device, vertices.Length * VertexPositionNormalColored.SizeInBytes, BufferUsage.WriteOnly);
            terrainVertexBuffer.SetData(vertices);

            terrainIndexBuffer = new IndexBuffer(device, typeof(int), indices.Length, BufferUsage.WriteOnly);
            terrainIndexBuffer.SetData(indices);
        }

        protected override void UnloadContent()
        {
        }

        protected override void Update(GameTime gameTime)
        {
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();


             float timeDifference = (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
             ProcessInput(timeDifference);
 
             base.Update(gameTime);
         }
 
         private void ProcessInput(float amount)
         {
             MouseState currentMouseState = Mouse.GetState();
             if (currentMouseState != originalMouseState)
             {
                 float xDifference = currentMouseState.X - originalMouseState.X;
                 float yDifference = currentMouseState.Y - originalMouseState.Y;
                 leftrightRot -= rotationSpeed * xDifference * amount;
                 updownRot -= rotationSpeed * yDifference * amount;
                 Mouse.SetPosition(device.Viewport.Width / 2, device.Viewport.Height / 2);
                 UpdateViewMatrix();
             }
 
             Vector3 moveVector = new Vector3(0, 0, 0);
             KeyboardState keyState = Keyboard.GetState();
             if (keyState.IsKeyDown(Keys.Up) || keyState.IsKeyDown(Keys.W))
                 moveVector += new Vector3(0, 0, -1);
             if (keyState.IsKeyDown(Keys.Down) || keyState.IsKeyDown(Keys.S))
                 moveVector += new Vector3(0, 0, 1);
             if (keyState.IsKeyDown(Keys.Right) || keyState.IsKeyDown(Keys.D))
                 moveVector += new Vector3(1, 0, 0);
             if (keyState.IsKeyDown(Keys.Left) || keyState.IsKeyDown(Keys.A))
                 moveVector += new Vector3(-1, 0, 0);
             if (keyState.IsKeyDown(Keys.Q))
                 moveVector += new Vector3(0, 1, 0);
             if (keyState.IsKeyDown(Keys.Z))
                 moveVector += new Vector3(0, -1, 0);
             AddToCameraPosition(moveVector * amount);
         }
 
         private void AddToCameraPosition(Vector3 vectorToAdd)
         {
             Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
             Vector3 rotatedVector = Vector3.Transform(vectorToAdd, cameraRotation);
             cameraPosition += moveSpeed * rotatedVector;
             UpdateViewMatrix();
         }
 
         private void UpdateViewMatrix()
         {
             Matrix cameraRotation = Matrix.CreateRotationX(updownRot) * Matrix.CreateRotationY(leftrightRot);
 
             Vector3 cameraOriginalTarget = new Vector3(0, 0, -1);
             Vector3 cameraOriginalUpVector = new Vector3(0, 1, 0);
 
             Vector3 cameraRotatedTarget = Vector3.Transform(cameraOriginalTarget, cameraRotation);
             Vector3 cameraFinalTarget = cameraPosition + cameraRotatedTarget;
 
             Vector3 cameraRotatedUpVector = Vector3.Transform(cameraOriginalUpVector, cameraRotation);
 
             viewMatrix = Matrix.CreateLookAt(cameraPosition, cameraFinalTarget, cameraRotatedUpVector);
         }
 
         protected override void Draw(GameTime gameTime)
         {
             float time = (float)gameTime.TotalGameTime.TotalMilliseconds / 100.0f;
             device.RenderState.CullMode = CullMode.None;
 
             device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.Black, 1.0f, 0);
             DrawTerrain(viewMatrix);
 
             base.Draw(gameTime);
         }
 
         private void DrawTerrain(Matrix currentViewMatrix)
         {
             effect.CurrentTechnique = effect.Techniques["Colored"];
             Matrix worldMatrix = Matrix.Identity;
             effect.Parameters["xWorld"].SetValue(worldMatrix);
             effect.Parameters["xView"].SetValue(currentViewMatrix);
             effect.Parameters["xProjection"].SetValue(projectionMatrix);
 
             effect.Parameters["xEnableLighting"].SetValue(true);
             effect.Parameters["xAmbient"].SetValue(0.4f);
             effect.Parameters["xLightDirection"].SetValue(new Vector3(-0.5f, -1, -0.5f));
 
             effect.Begin();
             foreach (EffectPass pass in effect.CurrentTechnique.Passes)
             {
                 pass.Begin();
 
                 device.Vertices[0].SetSource(terrainVertexBuffer, 0, VertexPositionNormalColored.SizeInBytes);
                 device.Indices = terrainIndexBuffer;
                 device.VertexDeclaration = terrainVertexDeclaration;
 
                 int noVertices = terrainVertexBuffer.SizeInBytes / VertexPositionNormalColored.SizeInBytes;
                 int noTriangles = terrainIndexBuffer.SizeInBytes / sizeof(int) / 3;
                 device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, noVertices, 0, noTriangles);
 
                 pass.End();
             }
             effect.End();
         }
     }
 }

发布时间:2009/12/9 上午9:53:58  阅读次数:5434

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号