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 阅读次数:5858