DirectX11学习笔记五 摄像机类

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

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 + 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);
  1. 透视类型
    其实就是透视矩阵
    在这里插入图片描述
    在这里插入图片描述
//定义
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

  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、θ和φ。

变量描述
rP到原点的距离
θ方位角,表示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中的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中的属性是怎么有效的与缓冲区交互呢?

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轴左转右转呗

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值