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