这一节跟渲染好像关系不大,但是值得看一看,我觉得更像是对游戏引擎和渲染的一些非常浅显的摸索和思考,以前用unity都是直接调的标准资源包里的摄像机类,很少从底层原理来考虑摄像机的实现方法。GameObject也是,unity都封装好了。
本节的demo里一共介绍了3种摄像机
先放一个以前用过的unity的非官方的第一人称幽灵视角摄像机类。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Control : MonoBehaviour {
public float zoomSensitivity = 10f;
public float mouseSensitivity = 5f;
public float speedSensitivity = 20f;
private float m_deltX = 0f;
private float m_deltY = 0f;
private Camera mainCamera;
void Start () {
mainCamera = GetComponent<Camera>();
}
// Update is called once per frame
void Update ()
{
if (Input.GetMouseButton(0))
{
LockCursor(true);
UFOMove();
ZoomMove();
}
else LockCursor(false);
}
private void FixedUpdate()
{
if (Input.GetMouseButton(0))
{
LookRotation();
}
}
private void ZoomMove()
{
if (Input.GetAxis("Mouse ScrollWheel") != 0)
{
mainCamera.transform.localPosition = mainCamera.transform.position + mainCamera.transform.forward * Input.GetAxis("Mouse ScrollWheel") * zoomSensitivity; ;
}
}
private void UFOMove()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
if (Input.GetKey(KeyCode.LeftShift))
{
horizontal *= 3; vertical *= 3;
}
mainCamera.transform.Translate(Vector3.forward * vertical * speedSensitivity * Time.deltaTime);
mainCamera.transform.Translate(Vector3.right * horizontal * speedSensitivity * Time.deltaTime);
}
private void LookRotation()
{
m_deltX += Input.GetAxis("Mouse X") * mouseSensitivity;
m_deltY -= Input.GetAxis("Mouse Y") * mouseSensitivity;
m_deltX = ClampAngle(m_deltX, -360, 360);
m_deltY = ClampAngle(m_deltY, -70, 70);
mainCamera.transform.rotation = Quaternion.Euler(m_deltY, m_deltX, 0);
}
private void LockCursor(bool b)
{
//Cursor.lockState = b ? CursorLockMode.Locked : Cursor.lockState = CursorLockMode.None;
Cursor.visible = b ? false : true;
}
float ClampAngle(float angle, float minAngle, float maxAgnle)
{
if (angle <= -360)
angle += 360;
if (angle >= 360)
angle -= 360;
return Mathf.Clamp(angle, minAngle, maxAgnle);
}
}
//原文链接:https://blog.csdn.net/MaxLykoS/article/details/72801979
unity中摄像机需要的一些有用的属性
只看重要的部分,首先得有Transform摄像机的坐标,位置旋转缩放,然后是,透视类型,FOV,剪裁距离和视口。
接下来就要写我们自己的摄像机类了,但是在写之前,得先弄清楚一些事情。
这个数学库实际上之前算世界变换矩阵的时候用到过,只不过当时只是矩阵计算,而在这里会添加一些向量计算。
可以参考这篇博客
做一些补充
前面提到,unity中摄像机的一些属性如下
摄像机的坐标属性,透视类型,FOV,剪裁距离和视口。
// 摄像机的观察空间坐标系对应在世界坐标系中的表示
DirectX::XMFLOAT3 m_Position;
DirectX::XMFLOAT3 m_Right;
DirectX::XMFLOAT3 m_Up;
DirectX::XMFLOAT3 m_Look;
坐标属性非常重要,关系到很多方法。
移动
平面步行
void FirstPersonCamera::Walk(float d)
{
XMVECTOR Pos = XMLoadFloat3(&m_Position);
XMVECTOR Right = XMLoadFloat3(&m_Right);
XMVECTOR Up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);
XMVECTOR Front = XMVector3Normalize(XMVector3Cross(Right, Up));
XMVECTOR Dist = XMVectorReplicate(d);
// DestPos = Dist * Front + SrcPos
XMStoreFloat3(&m_Position, XMVectorMultiplyAdd(Dist, Front, Pos));
//步行只能在同一水平面上,所以up是固定的,front是用up和right叉乘的
//最后d乘front再加上原位置即可
}
//调用,第一人称视角
if (keyState.IsKeyDown(Keyboard::W))
cam1st->Walk(dt * 3.0f);
if (keyState.IsKeyDown(Keyboard::S))
cam1st->Walk(dt * -3.0f);
UFO式飞行
void FirstPersonCamera::MoveForward(float d)
{
XMVECTOR Pos = XMLoadFloat3(&m_Position);
XMVECTOR Look = XMLoadFloat3(&m_Look);
XMVECTOR Dist = XMVectorReplicate(d);
// DestPos = Dist * Look + SrcPos
XMStoreFloat3(&m_Position, XMVectorMultiplyAdd(Dist, Look, Pos));
//look就是front,直接叠加即可
}
//调用 自由视角
if (keyState.IsKeyDown(Keyboard::W))
cam1st->MoveForward(dt * 3.0f);
if (keyState.IsKeyDown(Keyboard::S))
cam1st->MoveForward(dt * -3.0f);
左右移动
void FirstPersonCamera::Strafe(float d)
{
XMVECTOR Pos = XMLoadFloat3(&m_Position);
XMVECTOR Right = XMLoadFloat3(&m_Right);
XMVECTOR Dist = XMVectorReplicate(d);
// DestPos = Dist * Right + SrcPos
XMStoreFloat3(&m_Position, XMVectorMultiplyAdd(Dist, Right, Pos));
//相当于左右方向的飞行
}
//调用
if (keyState.IsKeyDown(Keyboard::A))
cam1st->Strafe(dt * -3.0f);
if (keyState.IsKeyDown(Keyboard::D))
cam1st->Strafe(dt * 3.0f);
视角移动
竖直方向
void FirstPersonCamera::Pitch(float rad)
{
XMMATRIX R = XMMatrixRotationAxis(XMLoadFloat3(&m_Right), rad);
XMVECTOR Up = XMVector3TransformNormal(XMLoadFloat3(&m_Up), R);
XMVECTOR Look = XMVector3TransformNormal(XMLoadFloat3(&m_Look), R);
float angle = asin(XMVectorGetY(Look)) * 180/XM_PI;
if (fabs(angle) > 80)
return;
XMStoreFloat3(&m_Up, Up);
XMStoreFloat3(&m_Look, Look);
//以right为轴,同时旋转up和look即可
}
//调用
cam1st->Pitch(mouseState.y * dt * 1.25f);
我把原来的代码改了一下,这样跟容易理解
另外,夹角限制不可以设为90,因为鼠标移速一快,rad一大,很容易就跳过90那个槛,摄像机直接朝向后方,反向<90。
水平方向
void FirstPersonCamera::RotateY(float rad)
{
XMMATRIX R = XMMatrixRotationY(rad);
XMStoreFloat3(&m_Right, XMVector3TransformNormal(XMLoadFloat3(&m_Right), R));
XMStoreFloat3(&m_Up, XMVector3TransformNormal(XMLoadFloat3(&m_Up), R));
XMStoreFloat3(&m_Look, XMVector3TransformNormal(XMLoadFloat3(&m_Look), R));
//三个轴同时按照y轴转动
}
//调用
cam1st->RotateY(mouseState.x * dt * 1.25f);
视图矩阵
上面那些对摄像机位置和轴等属性的修改,只是修改了摄像机类里的相应成员,要想在渲染上达到真正的修改效果,还是得从摄像机的视图矩阵入手。可以理解为,上面那些修改最终修改的是视图矩阵。
//定义
DirectX::XMFLOAT4X4 m_View;
//创建
void FirstPersonCamera::UpdateViewMatrix()
{
//XMStoreFloat4x4(&m_View, XMMatrixLookToLH(XMLoadFloat3(&m_Position), XMLoadFloat3(&m_Look),XMLoadFloat3(&m_Up)));
//或
XMVECTOR R = XMLoadFloat3(&m_Right);
XMVECTOR U = XMLoadFloat3(&m_Up);
XMVECTOR L = XMLoadFloat3(&m_Look);
XMVECTOR P = XMLoadFloat3(&m_Position);
// 保持摄像机的轴互为正交,且长度都为1
L = XMVector3Normalize(L);
U = XMVector3Normalize(XMVector3Cross(L, R));
// U, L已经正交化,需要计算对应叉乘得到R
R = XMVector3Cross(U, L);
// 填充观察矩阵
float x = -XMVectorGetX(XMVector3Dot(P, R));
float y = -XMVectorGetX(XMVector3Dot(P, U));
float z = -XMVectorGetX(XMVector3Dot(P, L));
XMStoreFloat3(&m_Right, R);
XMStoreFloat3(&m_Up, U);
XMStoreFloat3(&m_Look, L);
m_View = {
m_Right.x, m_Up.x, m_Look.x, 0.0f,
m_Right.y, m_Up.y, m_Look.y, 0.0f,
m_Right.z, m_Up.z, m_Look.z, 0.0f,
x, y, z, 1.0f
};
}
// 修改,用常量缓冲区保存,在vs里做乘法
m_CBFrame.view = XMMatrixTranspose(m_pCamera->GetViewXM());
......
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[1].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBChangesEveryFrame), &m_CBFrame, sizeof(CBChangesEveryFrame));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[1].Get(), 0);
//定义
DirectX::XMFLOAT4X4 m_Proj;
//创建
void Camera::SetFrustum(float fovY, float aspect, float nearZ, float farZ)
{
m_FovY = fovY; //FOV
m_Aspect = aspect;
m_NearZ = nearZ; //裁剪距离
m_FarZ = farZ; //裁剪距离
//这两个值就是算的三角形中间的两个Y方向的边长
m_NearWindowHeight = 2.0f * m_NearZ * tanf(0.5f * m_FovY);
m_FarWindowHeight = 2.0f * m_FarZ * tanf(0.5f * m_FovY);
XMStoreFloat4x4(&m_Proj, XMMatrixPerspectiveFovLH(m_FovY, m_Aspect, m_NearZ, m_FarZ));
}
//调用
// 初始化仅在窗口大小变动时修改的值
m_pCamera->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
m_CBOnResize.proj = XMMatrixTranspose(m_pCamera->GetProjXM());
.......
// 更新不容易被修改的常量缓冲区资源
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[2].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBChangesOnResize), &m_CBOnResize, sizeof(CBChangesOnResize));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[2].Get(), 0);
//最后再顶点着色器里做乘法
如果要改成正交投影的话,可以用XMMatrixOrthographicLH
//定义
D3D11_VIEWPORT m_ViewPort;
//创建
void Camera::SetViewPort(float topLeftX, float topLeftY, float width, float height, float minDepth, float maxDepth)
{
m_ViewPort.TopLeftX = topLeftX;
m_ViewPort.TopLeftY = topLeftY;
m_ViewPort.Width = width;
m_ViewPort.Height = height;
m_ViewPort.MinDepth = minDepth;
m_ViewPort.MaxDepth = maxDepth;
}
//调用
//源代码中定义了返回viewport的方法,但并没有使用,不过想调用也简单。
camera->SetViewPort(0.0f, 0.0f, (float)m_ClientWidth, (float)m_ClientHeight);
m_pd3dImmediateContext->RSSetViewports(1, &m_pCamera->GetViewPort());
D3D11_VIEWPORT的说明文档,前四个很好理解,最后两个“定义屏幕坐标z值在什么范围内能看见(不能看见就不渲染)”,好像暂时没什么用,设为0和1就好了。
XMFLOAT3 adjustedPos;
XMStoreFloat3(&adjustedPos, XMVectorClamp(cam1st->GetPositionXM(), XMVectorSet(-8.9f, 0.0f, -8.9f, 0.0f), XMVectorReplicate(8.9f)));
cam1st->SetPosition(adjustedPos);
XMVectorClamp函数用来限制向量的大小,向量是如何限制的呢?
//规则
XMVECTOR Result;
Result.x = min( max( V.x, Min.x ), Max.x );
Result.y = min( max( V.y, Min.y ), Max.y );
Result.z = min( max( V.z, Min.z ), Max.z );
Result.w = min( max( V.w, Min.w ), Max.w );
return Result;
第三人称摄像机独有的特点视角:鼠标控制的球形视角
需要用到的数据:目标位置,相机与目标距离,相机角度,最大最近距离(滚轮拉近)。
//摄像机初始化
cam3rd.reset(new ThirdPersonCamera);
cam3rd->SetFrustum(XM_PI / 3, AspectRatio(), 0.5f, 1000.0f);
m_pCamera = cam3rd;
XMFLOAT3 target = m_WoodCrate.GetPosition();
cam3rd->SetTarget(target);
cam3rd->SetDistance(8.0f);
cam3rd->SetDistanceMinMax(3.0f, 20.0f);
//每帧调用
cam3rd->SetTarget(m_WoodCrate.GetPosition());
// 绕物体旋转
cam3rd->RotateX(mouseState.y * dt * 1.25f);
cam3rd->RotateY(mouseState.x * dt * 1.25f);
cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);
主要需要关注的是Rotate方法,他定义了视角应该如何旋转。
球面坐标系
当然用xyz坐标也不是不行,摄像机位置可以用target - dist * Look = camPos来计算摄像机的位置,然后旋转Look轴,但是原博客里有提到这样会因为摄像机位置误差而出现抖动,不够平滑,所以使用球面坐标系。
球面坐标系有三个变量,r、θ和φ。
变量 | 描述 |
---|---|
r | P到原点的距离 |
θ | 方位角,表示P在x-y面上的投影与原点的连线和x轴之间所形成的夹角 |
φ | 极角,表示点P到原点的连线与z轴所构成的夹角,满足0<=φ<=π |
假设一个点P(x,y,z),用左手球面坐标可以表示为
x = r sinφ cosθ
y = r cosφ
z = r sinφ sinθ
坐标原点(0,0,0)与xyz轴的原点相同。
最后P+观察目标坐标,也就是将target作为球面坐标系原点,相当于平移坐标系,用来作为描述摄像机位置的一种办法。
Qx = Tx + r sinφ cosθ
Qy = Ty + r cosφ
Qz = Tz + r sinφ sinθ
视角移动
void ThirdPersonCamera::RotateX(float rad)
{
m_Phi -= rad;
// 将上下视野角度Phi限制在[pi/6, pi/2],
// 即余弦值[0, cos(pi/6)]之间
if (m_Phi < XM_PI / 6)
m_Phi = XM_PI / 6;
else if (m_Phi > XM_PIDIV2)
m_Phi = XM_PIDIV2;
}//代码很好理解
void ThirdPersonCamera::RotateY(float rad)
{
m_Theta = XMScalarModAngle(m_Theta - rad);
}
XMScalarModAngle函数,用来将角度限制在[-PI,PI],类似Clamp。不加限制也可以,但可能会让角度变得很大。
滚轮拉近
void ThirdPersonCamera::Approach(float dist)
{
m_Distance += dist;
// 限制距离在[m_MinDist, m_MaxDist]之间
if (m_Distance < m_MinDist)
m_Distance = m_MinDist;
else if (m_Distance > m_MaxDist)
m_Distance = m_MaxDist;
}//代码也是简单明了
视图矩阵
上面的所有修改最终都要落实到视图矩阵上来。
void ThirdPersonCamera::UpdateViewMatrix()
{
// 球面坐标系
float x = m_Target.x + m_Distance * sinf(m_Phi) * cosf(m_Theta);
float z = m_Target.z + m_Distance * sinf(m_Phi) * sinf(m_Theta);
float y = m_Target.y + m_Distance * cosf(m_Phi);
m_Position = { x, y, z };
XMVECTOR P = XMLoadFloat3(&m_Position);
XMVECTOR L = XMVector3Normalize(XMLoadFloat3(&m_Target) - P);
XMVECTOR R = XMVector3Normalize(XMVector3Cross(XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), L));
XMVECTOR U = XMVector3Cross(L, R);
// 更新向量
XMStoreFloat3(&m_Right, R);
XMStoreFloat3(&m_Up, U);
XMStoreFloat3(&m_Look, L);
m_View = {
m_Right.x, m_Up.x, m_Look.x, 0.0f,
m_Right.y, m_Up.y, m_Look.y, 0.0f,
m_Right.z, m_Up.z, m_Look.z, 0.0f,
-XMVectorGetX(XMVector3Dot(P, R)), -XMVectorGetX(XMVector3Dot(P, U)), -XMVectorGetX(XMVector3Dot(P, L)), 1.0f
};
}//基本原理跟第一人称的一样
在引入GameObject之前,势必要先学习下如何将缓冲区分开管理来提高运行效率,因为用GameObject来将渲染对象批量管理的话,肯定会引入很多管理的问题。
先复习一下常量缓冲区(Constant Buffer)在渲染第一帧前的使用方法,以下为例
struct CBChangesOnResize //在修改屏幕大小时修改
{
DirectX::XMMATRIX proj; //投影矩阵,用于VS
};
CBChangesOnResize m_CBOnResize; // 该缓冲区存放仅在窗口大小变化时更新的变量
ComPtr<ID3D11Buffer> m_pConstantBuffers[4]; // 常量缓冲区
// 设置常量缓冲区描述
D3D11_BUFFER_DESC cbd;
ZeroMemory(&cbd, sizeof(cbd));
cbd.Usage = D3D11_USAGE_DYNAMIC; // 表示允许动态修改
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; //允许CPU可读可写
// 新建用于VS和PS的常量缓冲区
cbd.ByteWidth = sizeof(CBChangesOnResize); //描述缓冲区大小
HR(m_pd3dDevice->CreateBuffer(&cbd, nullptr, m_pConstantBuffers[2].GetAddressOf()));
m_CBOnResize.proj = XMMatrixTranspose(m_pCamera->GetProjXM());
// 更新不容易被修改的常量缓冲区资源
D3D11_MAPPED_SUBRESOURCE mappedData;
HR(m_pd3dImmediateContext->Map(m_pConstantBuffers[2].Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData));
memcpy_s(mappedData.pData, sizeof(CBChangesOnResize), &m_CBOnResize, sizeof(CBChangesOnResize));
m_pd3dImmediateContext->Unmap(m_pConstantBuffers[2].Get(), 0);
m_pd3dImmediateContext->VSSetConstantBuffers(2, 1, m_pConstantBuffers[2].GetAddressOf()); // 槽号为2
cbuffer CBChangesOnResize : register(b2)
{
matrix g_Proj;
}
// 顶点着色器(3D)
VertexPosHWNormalTex VS_3D(VertexPosNormalTex vIn)
{
VertexPosHWNormalTex vOut;
matrix viewProj = mul(g_View, g_Proj);
float4 posW = mul(float4(vIn.PosL, 1.0f), g_World);
vOut.PosH = mul(posW, viewProj);
vOut.PosW = posW.xyz;
vOut.NormalW = mul(vIn.NormalL, (float3x3) g_WorldInvTranspose);
vOut.Tex = vIn.Tex;
return vOut;
}
回到正题,根据可能需要更新的频率分组,一共有四种常量缓冲区
struct CBChangesEveryDrawing //于DrawScene()更新,用于VS
{
DirectX::XMMATRIX world; // 定义物体的坐标,缩放和旋转。相当于unity中的Transform
DirectX::XMMATRIX worldInvTranspose;// 用来加速计算法线变换
};
struct CBChangesEveryFrame //于UpdateScene()更新,用于VS和PS
{
DirectX::XMMATRIX view; //视图矩阵,用于MVP,VS
DirectX::XMFLOAT4 eyePos; //摄像机位置,用来计算光照,PS
};
//先执行UpdateScene(),然后再DrawScene()
struct CBChangesOnResize //于OnResize()更新,用于VS
{
DirectX::XMMATRIX proj; //投影矩阵,用于MVP变换
};
struct CBChangesRarely //(暂时)不更新,只初始化,用于PS
{
DirectionalLight dirLight[10];
PointLight pointLight[10];
SpotLight spotLight[10];
Material material;
int numDirLight;
int numPointLight;
int numSpotLight;
float pad; // 打包保证16字节对齐
};
光源初始化
// 初始化不会变化的值
// 环境光
m_CBRarely.dirLight[0].ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_CBRarely.dirLight[0].diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
m_CBRarely.dirLight[0].specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_CBRarely.dirLight[0].direction = XMFLOAT3(0.0f, -1.0f, 0.0f);
// 灯光
m_CBRarely.pointLight[0].position = XMFLOAT3(0.0f, 10.0f, 0.0f);
m_CBRarely.pointLight[0].ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_CBRarely.pointLight[0].diffuse = XMFLOAT4(0.8f, 0.8f, 0.8f, 1.0f);
m_CBRarely.pointLight[0].specular = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_CBRarely.pointLight[0].att = XMFLOAT3(0.0f, 0.1f, 0.0f);
m_CBRarely.pointLight[0].range = 25.0f;
m_CBRarely.numDirLight = 1;
m_CBRarely.numPointLight = 1;
m_CBRarely.numSpotLight = 0;
// 初始化材质
m_CBRarely.material.ambient = XMFLOAT4(0.5f, 0.5f, 0.5f, 1.0f);
m_CBRarely.material.diffuse = XMFLOAT4(0.6f, 0.6f, 0.6f, 1.0f);
m_CBRarely.material.specular = XMFLOAT4(0.1f, 0.1f, 0.1f, 50.0f);
但是,渲染一个物体还需要顶点属性,纹理和纹理采样器,那么GameObject中的属性是怎么有效的与缓冲区交互呢?
定义方法如下
// 一个尽可能小的游戏对象类
class GameObject
{
public:
GameObject();
// 获取位置
DirectX::XMFLOAT3 GetPosition() const;
// 设置缓冲区
template<class VertexType, class IndexType>
void SetBuffer(ID3D11Device * device, const Geometry::MeshData<VertexType, IndexType>& meshData);
// 设置纹理
void SetTexture(ID3D11ShaderResourceView * texture);
// 设置矩阵
void SetWorldMatrix(const DirectX::XMFLOAT4X4& world);
void XM_CALLCONV SetWorldMatrix(DirectX::FXMMATRIX world);
// 绘制
void Draw(ID3D11DeviceContext * deviceContext);
// 设置调试对象名
// 若缓冲区被重新设置,调试对象名也需要被重新设置
void SetDebugObjectName(const std::string& name);
private:
DirectX::XMFLOAT4X4 m_WorldMatrix; // 世界矩阵
ComPtr<ID3D11ShaderResourceView> m_pTexture; // 纹理
ComPtr<ID3D11Buffer> m_pVertexBuffer; // 顶点缓冲区
ComPtr<ID3D11Buffer> m_pIndexBuffer; // 索引缓冲区
UINT m_VertexStride; // 顶点字节大小
UINT m_IndexCount; // 索引数目
};
后面有关GameObject的操作以人为的逻辑居多,就不一一分析了,以后用到的时候可以再反向思考。
按照惯例引用原博客的问题
解
//UpdateScene()
else if (m_CameraMode == CameraMode::ThirdPerson)
{
// 第三人称摄像机的操作
cam3rd->SetTarget(m_WoodCrate.GetPosition());
// 绕物体旋转
cam3rd->RotateX(mouseState.y * dt * 1.25f);
cam3rd->RotateY(mouseState.x * dt * 1.25f);
cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);
if (keyState.IsKeyDown(Keyboard::W))
{
cam3rd->WalkTarget(dt * 3.0f);
}
if (keyState.IsKeyDown(Keyboard::S))
{
cam3rd->WalkTarget(dt * -3.0f);
}
if (keyState.IsKeyDown(Keyboard::A))
{
cam3rd->StrafeTarget(dt * -3.0f);
}
if (keyState.IsKeyDown(Keyboard::D))
{
cam3rd->StrafeTarget(dt * 3.0f);
}
// 将位置限制在[-8.9f, 8.9f]的区域内
// 不允许穿地
XMFLOAT3 adjustedPos;
XMStoreFloat3(&adjustedPos, XMVectorClamp(cam3rd->GetTargetPositionXM(), XMVectorSet(-8.9f, 0.0f, -8.9f, 0.0f), XMVectorReplicate(8.9f)));
m_WoodCrate.SetWorldMatrix(XMMatrixTranslation(adjustedPos.x, adjustedPos.y, adjustedPos.z));
}
//相关函数定义,跟第一人称摄像机类似
void ThirdPersonCamera::WalkTarget(float d)
{
XMStoreFloat3(&m_Target, XMVectorMultiplyAdd(XMVectorReplicate(d), XMVector3Cross(XMLoadFloat3(&m_Right), XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f)), XMLoadFloat3(&m_Target)));
}
void ThirdPersonCamera::StrafeTarget(float d)
{
XMStoreFloat3(&m_Target, XMVectorMultiplyAdd(XMVectorReplicate(d), XMLoadFloat3(&m_Right), XMLoadFloat3(&m_Target)));
}
DirectX::XMVECTOR ThirdPersonCamera::GetTargetPositionXM() const
{
return XMLoadFloat3(&m_Target);
}
else if (m_CameraMode == CameraMode::ThirdPerson)
{
// 第三人称摄像机的操作
cam3rd->SetTarget(m_WoodCrate.GetPosition());
// 绕物体旋转
cam3rd->RotateX(mouseState.y * dt * 1.25f);
cam3rd->RotateY(mouseState.x * dt * 1.25f);
cam3rd->Approach(-mouseState.scrollWheelValue / 120 * 1.0f);
static XMFLOAT3 rotate(0, 0, XM_PIDIV2);
if (keyState.IsKeyDown(Keyboard::W))
{
cam3rd->WalkTarget(dt * 3.0f,rotate.y);
rotate.x += dt;
}
if (keyState.IsKeyDown(Keyboard::S))
{
cam3rd->WalkTarget(dt * -3.0f, rotate.y);
rotate.x -= dt;
}
if (keyState.IsKeyDown(Keyboard::A))
{
//cam3rd->StrafeTarget(dt * -3.0f);
rotate.y -= dt;
}
if (keyState.IsKeyDown(Keyboard::D))
{
//cam3rd->StrafeTarget(dt * 3.0f);
rotate.y += dt;
}
// 将位置限制在[-8.9f, 8.9f]的区域内
// 不允许穿地
XMFLOAT3 adjustedPos;
XMStoreFloat3(&adjustedPos, XMVectorClamp(cam3rd->GetTargetPositionXM(), XMVectorSet(-8.9f, 0.0f, -8.9f, 0.0f), XMVectorReplicate(8.9f)));
XMMATRIX r = XMMatrixRotationZ(rotate.z) * XMMatrixRotationX(rotate.x) * XMMatrixRotationY(rotate.y);
m_WoodCrate.SetWorldMatrix(XMMatrixMultiply(r, XMMatrixTranslation(adjustedPos.x, adjustedPos.y, adjustedPos.z)));
}
旋转的部分都没什么难度,重点是前后移动的部分。
void ThirdPersonCamera::WalkTarget(float d,float angle)
{
XMVECTOR front = XMVectorSet(0, 0, 1, 0);
XMVECTOR up = XMVectorSet(0, 1, 0, 0);
auto r = cosf(angle) * front;
r = r + (1 - cosf(angle)) * XMVector3Dot(front, up) * up;
r = r + (sinf(angle)) * XMVector3Cross(up, front);
r = XMVector3Normalize(r);
XMStoreFloat3(&m_Target, XMVectorMultiplyAdd(XMVectorReplicate(d), r, XMLoadFloat3(&m_Target)));
}
套用的罗德里格旋转公式,坐标轴用世界坐标,圆柱躺下后,朝前的是z轴(世界坐标,不随圆柱旋转而变化),那就将z轴绕y轴旋转角度,修改z轴朝向,并单位化即可,本质上是一种向量的旋转。
3. 那就按Look轴左转右转呗