这一节跟渲染好像关系不大,但是值得看一看,我觉得更像是对游戏引擎和渲染的一些非常浅显的摸索和思考,以前用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 framevoid 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,剪裁距离和视口。
接下来就要写我们自己的摄像机类了,但是在写之前,得先弄清楚一些事情。

DirectXMath数学库

这个数学库实际上之前算世界变换矩阵的时候用到过,只不过当时只是矩阵计算,而在这里会添加一些向量计算。
可以参考这篇博客
做一些补充

  1. XMVECTOR和XMFloat3
    首先XMVECTOR里有很多属性,但是看float那一行就够了。其次,XMVECTOR的float有4个数,而XMFloat3有三个数,当然可以用XMFloat4,但是很多情况下用3就够了。另外,XMFloat3这种类似的类型只是用来赋值存储,不能用于计算,用于计算的是XMVECTOR。
    那二者如何转化呢?
    float3 to vector -> XMLoadFloat3(float3)
    vector to float3 -> XMStoreFloat3(float3, vector)
    matrix to float4x4 -> XMStoreFloat4x4(float4x4, matrix)
    float4x4 to matrix -> XMLoadFloat4x4(float4x4)
    取单值
    float3.x
    XMVectorGetX(vector)
    最后,假如两个vector点乘,结果仍是vector(应该是个常数),但是这个vector的xyzw都一样(都是结果常数)。
  2. XMVector3Transform和XMVector3TransformNormal
    具体可以查微软文档,其中前者将认为输入向量的w为1,而返回的向量w可能不为1。后者将会忽略矩阵的最后一行计算。
  3. 常数乘向量时,要把常数保存到xyzw都一样的vector中再跟其他vector计算,用XMVectorReplicate函数。

实现第一人称Camera

前面提到,unity中摄像机的一些属性如下
摄像机的坐标属性,透视类型,FOV,剪裁距离和视口。

  1. 坐标属性
 // 摄像机的观察空间坐标系对应在世界坐标系中的表示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 + SrcPosXMStoreFloat3(&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 + SrcPosXMStoreFloat3(&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 + SrcPosXMStoreFloat3(&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);// 保持摄像机的轴互为正交,且长度都为1L = XMVector3Normalize(L);U = XMVector3Normalize(XMVector3Cross(L, R));// U, L已经正交化,需要计算对应叉乘得到RR = 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);
  1. 透视类型
    其实就是透视矩阵

//定义
DirectX::XMFLOAT4X4 m_Proj;
//创建
void Camera::SetFrustum(float fovY, float aspect, float nearZ, float farZ)
{m_FovY = fovY;  //FOVm_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

  1. 视口
    视口没什么复杂的概念,就是定义渲染的画面所呈现在屏幕上的大小和位置。
//定义
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就好了。

  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θ
视角移动

  1. 视角上下移动
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;
}//代码很好理解
  1. 视角左右移动
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};
  1. 定义结构体和声明结构体对象
 CBChangesOnResize m_CBOnResize;                             // 该缓冲区存放仅在窗口大小变化时更新的变量
  1. 声明并用常量缓冲区描述创建常量缓冲区对象
 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()));
  1. 修改(或初始化)结构体对象
 m_CBOnResize.proj = XMMatrixTranspose(m_pCamera->GetProjXM());
  1. 将结构体对象映射到缓冲区对象
// 更新不容易被修改的常量缓冲区资源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);
  1. 将缓冲区绑定到渲染管线
    透视矩阵主要用于MVP变换,在顶点着色器中使用。
m_pd3dImmediateContext->VSSetConstantBuffers(2, 1, m_pConstantBuffers[2].GetAddressOf()); // 槽号为2
  1. 在着色器中定义相关的缓冲区
cbuffer CBChangesOnResize : register(b2)
{matrix g_Proj;
}
  1. 在着色器中使用
// 顶点着色器(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中的TransformDirectX::XMMATRIX worldInvTranspose;// 用来加速计算法线变换};struct CBChangesEveryFrame  //于UpdateScene()更新,用于VS和PS{DirectX::XMMATRIX view;  //视图矩阵,用于MVP,VSDirectX::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中的属性是怎么有效的与缓冲区交互呢?

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的操作以人为的逻辑居多,就不一一分析了,以后用到的时候可以再反向思考。

思考

按照惯例引用原博客的问题

  1. 在第三人称模式下,让物体也能够进行前后、左右的平移运动
  2. 在第三人称模式下,使用平躺的圆柱体,让其左右平移运动改为左右旋转运动,前后运动改为朝前滚动
  3. 在第一人称模式下,给摄像机添加对"滚动"的支持(绕Look轴旋转)

  1. 我一开始又走弯路了。
    当前代码中第三人称摄像机与其他摄像机有些不同,第三人称摄像机的位置是用球面坐标算出来的,围绕的中心是m_target,所以我们在修改第三人称摄像机坐标的时候不应该修改摄像机的坐标,而是应该修改围绕的中心的坐标。
 //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);
}
  1. 因为当前项目中定义游戏物体位置旋转缩放信息的属性只有一个世界矩阵,不像unity那样对三维有保存,所以要处理逐渐旋转的话只能像一开始旋转立方体那样用旋转矩阵叠加的方法,或者自己写一个保存,每次修改就好了。
 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轴左转右转呗

DirectX11学习笔记五 摄像机类相关推荐

  1. Directx11学习笔记【二】 将HelloWin封装成类

    我们把上一个教程的代码封装到一个类中来方便以后的使用. 首先新建一个空工程叫做MyHelloWin,添加一个main.cpp文件,然后新建一个类叫做MyWindow,将于窗体有关的操作封装到里面 My ...

  2. python函数是一段具有特定功能的语句组_Python学习笔记(五)函数和代码复用

    本文将为您描述Python学习笔记(五)函数和代码复用,具体完成步骤: 函数能提高应用的模块性,和代码的重复利用率.在很多高级语言中,都可以使用函数实现多种功能.在之前的学习中,相信你已经知道Pyth ...

  3. StackExchange.Redis学习笔记(五) 发布和订阅

    StackExchange.Redis学习笔记(五) 发布和订阅 原文:StackExchange.Redis学习笔记(五) 发布和订阅 Redis命令中的Pub/Sub Redis在 2.0之后的版 ...

  4. 吴恩达《机器学习》学习笔记五——逻辑回归

    吴恩达<机器学习>学习笔记五--逻辑回归 一. 分类(classification) 1.定义 2.阈值 二. 逻辑(logistic)回归假设函数 1.假设的表达式 2.假设表达式的意义 ...

  5. Mr.J-- jQuery学习笔记(七)--CSS类操作文本值操作

    不了解属性以及属性操作的同学可以看我之前的博客:Mr.J-- jQuery学习笔记(五)--属性及属性节点 下面demo 中btn用到的角标,之前写验证码动态强度测试时也用过同样方法:Mr.J--密码 ...

  6. Spring Boot 框架学习笔记(五)( SpringSecurity安全框架 )

    Spring Boot 框架学习笔记(五) SpringSecurity安全框架 概述 作用 开发示例: 1. 新建项目 2. 引入依赖 3. 编写`SecurityConfig`类,实现认证,授权, ...

  7. Polyworks脚本开发学习笔记(五)-变量使用基本语法

    Polyworks脚本开发学习笔记(五)-变量使用基本语法 定义变量及赋值 定义各种类型的变量 定义变量时,只需要使用DECLARE 关键字即可定义,为了区别变量和脚本中的其它字符,建议都以小写v开头 ...

  8. 【K210】K210学习笔记五——串口通信

    [K210]K210学习笔记五--串口通信 前言 K210如何进行串口通信 K210串口配置 K210串口发送相关定义 K210串口接收相关定义 K210串口发送接收测试 完整源码 前言 本人大四学生 ...

  9. Java之多线程学习笔记五 —— 多线程模拟龟兔赛跑

    Java之多线程学习笔记五 -- 多线程模拟龟兔赛跑 参考教程B站狂神https://www.bilibili.com/video/BV1V4411p7EF package pers.ylw.less ...

  10. muduo网络库学习笔记(五) 链接器Connector与监听器Acceptor

    目录 muduo网络库学习笔记(五) 链接器Connector与监听器Acceptor Connector 系统函数connect 处理非阻塞connect的步骤: Connetor时序图 Accep ...

最新文章

  1. ref out 的使用
  2. mapReducer第一个例子WordCount
  3. 平舌音 Z C S 的正确发音方式
  4. sql中的遇到的有问题的
  5. 28 SD配置-主数据-信用管理-定义信贷组
  6. 使用 JS刷新框架子页面
  7. unity, itween 对不透明对象使用FadeTo需要先更换material
  8. 码支付如何对接网站_做“刷脸支付”怎么推广?怎么办理刷脸支付POS机?
  9. Chrome 新功能:因更新或崩溃而重启后,PWA应用将自动恢复运行!
  10. 教你轻松快速学会用Calibre TXT转MOBI
  11. w ndows安装程序无法将,windows安装程序无法将windows配置在硬件上运行
  12. cups linux 升级_linux cups版本
  13. 很遗憾,这就是现实!35岁之后软件测试工程师靠什么养家?
  14. JS - 将tree(树形)数据结构格式改为一维数组对象格式(扁平化)
  15. oscp——five86-1
  16. 2014工作总结与2015展望
  17. baidumaptrace.php,GitHub - SignalLine/BaiduMapTrace: 百度地图运动轨迹纠偏、去噪、绑路之百度鹰眼sdk服务...
  18. linux下代码写错了怎么更改_谢宝友:手把手教你给Linux内核发patch
  19. Dijkstra算法总结
  20. networkx库整理

热门文章

  1. CAD参数绘制直线(网页版)
  2. 24V电压TVS二极管选型
  3. javascript 中时区知识的整理 UTC GMT
  4. 平安普惠java面试_【面经】平安普惠开发工程师面试
  5. 2020滑铁卢大学计算机科学学费,滑铁卢大学专业
  6. linux修改重传次数,《关于TCP SYN包的超时与重传》——那些你应该知道的知识(四)...
  7. 自考本科的单科成绩包括英语和计算机吗,自考问题自考统考科目(例如:英语,计算机等 – 手机爱问...
  8. java山海经之轩辕_山海经之情剑轩辕 炼化任务详细攻略
  9. CK-GW06-E01与恩基士PLC配置指南
  10. python苹果手机触摸_python实现查询苹果手机维修进度