引言
本Camera 类是一个为3D图形应用设计的摄像机控制系统,主要用于DirectX 9环境。
它包含了一系列方法和属性,用于控制和更新3D视图和投影矩阵。以下是对该类各个部分的详细分析,旨在帮助各位更好地理解其设计和功能。
本文不会讲解关于矩阵计算的基本内容,请先行了解。
MVP变换
视图矩阵View
首先要注意的是,在DX的结构中,基向量由行向量来表示,故而此处摄像机在世界空间下的旋转矩阵为
视图变换:把物体从世界空间转换到视图空间,即获得顶点在视图坐标系中的坐标,也可以理解为获得顶点相对于摄像机的位置。
要注意的是,视图变换是先平移再旋转,故而旋转矩阵的格式应为
透视投影矩阵
强烈推荐听这位up主的教程,可视化做的非常的好,也很简明易懂。
实现代码讲解
类成员变量
D3DXMATRIX m_View; - 存储视图矩阵,控制摄像机的方向和位置。 D3DXMATRIX m_Project; - 存储投影矩阵,定义如何将3D场景映射到2D屏幕。 D3DXVECTOR3 m_Pos; - 摄像机在世界坐标中的位置。 D3DXVECTOR3 m_LookAt; - 摄像机视点的目标位置。 D3DXVECTOR3 m_Up; - 定义摄像机的上方向。 D3DXVECTOR3 m_Right; - 摄像机的右向量,由上向量和前向向量叉乘得到。 D3DXVECTOR3 m_Direction; - 摄像机的前向向量,指向摄像机正前方。 浮点数 m_fNear, m_fFar, m_fFOV, m_fAspect; - 控制投影矩阵的参数,分别表示近裁剪面、远裁剪面、视场角度和宽高比。 bool m_bIsRot; - 指示是否正在进行旋转操作。 float m_fYaw, m_fPitch; - 控制摄像机的偏航和俯仰角度。 POINT m_LastPoint; - 存储上一次鼠标位置,用于计算鼠标移动距离。 D3DXVECTOR3 upVector, directionVector; - 辅助变量,用于旋转计算。
类方法
Camera() - 构造函数,初始化成员变量,设置默认值。 SetViewParam(D3DXVECTOR3&, D3DXVECTOR3&, D3DXVECTOR3&) - 设置视图矩阵参数,包括位置、视点和上方向。 SetProjMatrix(float, float, float, float) - 设置投影矩阵参数,包括视场角、纵横比、近裁剪面和远裁剪面。 GetViewMatrix() - 返回当前视图矩阵的指针。 GetProjMatrix() - 返回当前投影矩阵的指针。 Update(float) - 根据时间差更新摄像机位置和方向,处理键盘输入以移动摄像机。 HandleMessage(HWND, UINT, WPARAM, LPARAM) - 处理窗口消息,例如鼠标点击和移动,用于控制摄像机的旋转。
功能分析
摄像机控制:通过 Update 方法实现基于时间的平滑移动和旋转,HandleMessage 方法处理用户输入,调整偏航和俯仰角。
视图和投影矩阵更新:每次摄像机位置或方向改变时,都会重新计算视图矩阵,保证渲染的视图与摄像机状态同步。
用户交互:通过鼠标和键盘与摄像机交互,提供直观的第一人称视角操作。
这个类的设计充分利用了DirectX的功能来实现一个灵活且功能强大的3D摄像机系统,非常适合用于游戏开发或其他需要实时3D视图控制的应用。
摄像机旋转和键盘操作
摄像机的旋转处理通过监测鼠标移动来实现。
当用户按下左键并移动鼠标时,通过计算从上一次鼠标位置到当前位置的变化,来调整摄像机的偏航和俯仰角度。
这种交互方式模拟了许多第一人称游戏中的摄像机控制机制,允许用户以自然的方式查看场景。
LRESULT Camera::HandleMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_LBUTTONDOWN: m_bIsRot = true; // 开始旋转 SetCapture(hWnd); // 捕获鼠标 GetCursorPos(&m_LastPoint); // 记录当前鼠标位置 break; case WM_LBUTTONUP: m_bIsRot = false; // 停止旋转 ReleaseCapture(); // 释放鼠标 break; case WM_MOUSEMOVE: if (m_bIsRot) { POINT currentPos; GetCursorPos(¤tPos); // 获取当前鼠标位置 m_fYaw = (currentPos.x - m_LastPoint.x) * 0.01f; // 根据鼠标水平移动调整偏航角 m_fPitch = (currentPos.y - m_LastPoint.y) * 0.01f; // 根据鼠标垂直移动调整俯仰角 m_fPitch = max(-D3DX_PI / 2, min(D3DX_PI / 2, m_fPitch)); // 限制俯仰角度以避免翻转 m_LastPoint = currentPos; // 更新鼠标位置 } break; } return 0; }
更新视图矩阵
Update 方法中更新的视图矩阵确保摄像机的方向变化能够即时反映在渲染的场景中。
这是通过创建一个旋转矩阵来调整摄像机的上向量和前向向量,并使用这些向量来更新 LookAt 矩阵完成的。
LRESULT Camera::HandleMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_LBUTTONDOWN: m_bIsRot = true; // 开始旋转 SetCapture(hWnd); // 捕获鼠标 GetCursorPos(&m_LastPoint); // 记录当前鼠标位置 break; case WM_LBUTTONUP: m_bIsRot = false; // 停止旋转 ReleaseCapture(); // 释放鼠标 break; case WM_MOUSEMOVE: if (m_bIsRot) { POINT currentPos; GetCursorPos(¤tPos); // 获取当前鼠标位置 m_fYaw += (currentPos.x - m_LastPoint.x) * 0.01f; // 根据鼠标水平移动调整偏航角 m_fPitch += (currentPos.y - m_LastPoint.y) * 0.01f; // 根据鼠标垂直移动调整俯仰角 m_fPitch = max(-D3DX_PI / 2, min(D3DX_PI / 2, m_fPitch)); // 限制俯仰角度以避免翻转 m_LastPoint = currentPos; // 更新鼠标位置 } break; } return 0; }
这个 Camera 类的设计结合了灵活的视角控制与直观的用户输入响应,使其成为3D图形应用中非常有用的组件。它提供了一种有效的方法来模拟真实世界的相机动作,通过简单的键盘和鼠标操作即可实现复杂的视角转换,非常适合用于游戏开发和3D场景导览等应用场景。
Camera类实现逻辑梳理
定义类,对类进行初始化 ⇒ 计算视图矩阵和透视投影矩阵 ⇒ 获取视图矩阵以及透视投影矩阵的地址 ⇒ 实时获取鼠标信息 ⇒ 根据鼠标信息进行动态更新,反复更新视图矩阵
源码
camera类
#pragma once //保证头文件只被编译一次,防止头文件被重复引用。 #include <windows.h> #include <d3dx9.h> class Camera { private: D3DXMATRIX m_View; // 视图变换矩阵 D3DXMATRIX m_Project; // 投影变换矩阵 D3DXVECTOR3 m_Pos; // 位置 D3DXVECTOR3 m_LookAt; // 视点 D3DXVECTOR3 m_Up; // 正方向 Y D3DXVECTOR3 m_Right; // 右向量 X D3DXVECTOR3 m_Direction;// 朝向 Z float m_fNear; // 近平面 float m_fFar; // 远平面 float m_fFOV; // FOV夹角 float m_fAspect; // 纵横比 bool m_bIsRot; // 是否发生旋转 float m_fYaw; // 偏航角 float m_fPitch; // 俯仰角 POINT m_LastPoint; // 上一次的鼠标位置 D3DXVECTOR3 upVector; D3DXVECTOR3 directionVector; public: Camera(); //设置视图矩阵参数 void SetViewParam(D3DXVECTOR3& vPos, D3DXVECTOR3& vLookAt, D3DXVECTOR3& vUp); //设置透视投影矩阵 void SetProjMatrix(float fFov, float fAspect, float fNear, float fFar); //获取视图矩阵的地址 D3DXMATRIX* GetViewMatrix(); //获取透视投影矩阵的地址 D3DXMATRIX* GetProjMatrix(); //设置数据更新函数 void Update(float deltaTime); //设置消息获取函数,此处用来获取鼠标消息 LRESULT HandleMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); }; //Camera的构造函数,定义初始参数 Camera::Camera() : m_fYaw(0), m_fPitch(0),m_bIsRot(false), upVector(0, 1, 0), directionVector(0, 0, 1) //偏航角,俯仰角,旋转关闭,正方向010,朝向001 { ZeroMemory(&m_LastPoint, sizeof(POINT));//清空点击位置的储存区 } //SetViewParam的构造函数 void Camera::SetViewParam(D3DXVECTOR3& vPos, D3DXVECTOR3& vLookAt, D3DXVECTOR3& vUp) { m_Pos = vPos; m_LookAt = vLookAt; m_Up = vUp;//获取矩阵的3个参数 m_Direction = vLookAt - vPos;//计算m_Direction 方向(Z轴) D3DXVec3Normalize(&m_Direction, &m_Direction);//归一化,将第二个参数的值归一化传给第一个参数 D3DXVec3Cross(&m_Right, &vUp, &m_Direction);//后俩个数叉乘的值传给Right,得到右方向(X轴) D3DXMatrixLookAtLH(&m_View, &m_Pos, &m_LookAt, &m_Up);//计算视图矩阵 } //透视投影矩阵的构造函数,参数为FOV夹角,纵横比,近平面,远平面 void Camera::SetProjMatrix(float fFov, float fAspect, float fNear, float fFar) { m_fFOV = fFov; m_fAspect = fAspect; m_fNear = fNear; m_fFar = fFar;//获取矩阵的4个参数 D3DXMatrixPerspectiveFovLH(&m_Project, fFov, fAspect, fNear, fFar); }//计算透视投影矩阵 //GetViewMatrix的构造函数 D3DXMATRIX* Camera::GetViewMatrix() { return &m_View;//返回视图矩阵 } //GetProjMatrix的构造函数 D3DXMATRIX* Camera::GetProjMatrix() { return &m_Project;//返回透视投影矩阵 } //Update的构造函数,用于更新数据 void Camera::Update(float deltaTime) { //如果m_bIsRot为1 if (m_bIsRot) { //设置中间变量vUp和vDirection D3DXVECTOR3 vUp, vDirection; D3DXMATRIX matCamera; //生成具有指定偏航、俯仰和滚动的矩阵,最后一个参数代表绕z轴的旋转 //这个矩阵是经过旋转之后的新坐标系的变换矩阵,向量与该矩阵相乘则从新坐标系跳转到世界坐标系 D3DXMatrixRotationYawPitchRoll(&matCamera, m_fYaw, m_fPitch, 0); //用matCamera矩阵转换upVector向量(单位向量010),归一化后传给vUp //D3DXVec3TransformCoord输入的是一个三维向量与4维矩阵 //3维向量通过齐次变换后进行计算后再归一化后返回一个3维向量 D3DXVec3TransformCoord(&vUp, &upVector, &matCamera); //用matCamera矩阵转化directionVector向量(单位向量001),归一化后传给vDirection D3DXVec3TransformCoord(&vDirection, &directionVector, &matCamera); //计算更新后的视点 m_LookAt = m_Pos + vDirection; //更新视图矩阵 D3DXMatrixLookAtLH(&m_View, &m_Pos, &m_LookAt, &vUp); //将新的Direction向量覆盖旧值 m_Direction = m_LookAt - m_Pos; //归一化,确保不会出现精度上的误差 D3DXVec3Normalize(&m_Direction, &m_Direction); //赋值,计算新的m_Right m_Up = vUp; D3DXVec3Cross(&m_Right, &m_Up, &m_Direction); } //新定义一个前进向量 D3DXVECTOR3 forward = m_LookAt - m_Pos; //归一化处理 D3DXVec3Normalize(&forward, &forward); //新定义一个右向量并赋值与归一化处理 D3DXVECTOR3 right; D3DXVec3Cross(&right, &m_Up, &forward); D3DXVec3Normalize(&right, &right); //设置速度,deltaTime为电脑一帧的间隔时间 float speed = 5.0f * deltaTime; //按下键盘移动m_Pos位置,0x8000表示32767,即2进制的1000 0000 0000 0000,可以省略 if (GetAsyncKeyState('W') & 0x8000) m_Pos += forward * speed; if (GetAsyncKeyState('S') & 0x8000) m_Pos -= forward * speed; if (GetAsyncKeyState('A') & 0x8000) m_Pos -= right * speed; if (GetAsyncKeyState('D') & 0x8000) m_Pos += right * speed; m_LookAt = m_Pos + forward; // 重新计算 LookAt 点 //更新视图矩阵 D3DXMatrixLookAtLH(&m_View, &m_Pos, &m_LookAt, &m_Up); } //获取鼠标信息 LRESULT Camera::HandleMessage(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { //鼠标按下 case WM_LBUTTONDOWN: //允许旋转 m_bIsRot = true; //将鼠标捕获设置为属于当前线程的指定窗口。 SetCapture(hWnd); //获取鼠标位置 GetCursorPos(&m_LastPoint); break; //鼠标松开 case WM_LBUTTONUP: //关闭旋转 m_bIsRot = false; //从当前线程中的窗口释放鼠标捕获,并还原正常鼠标输入处理。 ReleaseCapture(); break; //鼠标移动 case WM_MOUSEMOVE: //m_bIsRot为真,即鼠标按下时 if (m_bIsRot) { //创建当前的位置变量 POINT currentPos; //获取鼠标位置信息 GetCursorPos(¤tPos); //计算偏航角和俯仰角 m_fYaw += (currentPos.x - m_LastPoint.x) * 0.01f; m_fPitch += (currentPos.y - m_LastPoint.y) * 0.01f; //限制俯仰角范围在(-90°,90°)之间 m_fPitch = max(-D3DX_PI / 2, min(D3DX_PI / 2, m_fPitch)); // Limit pitch //更新上一帧的鼠标位置 m_LastPoint = currentPos; } break; } return 0; }
WinMain
#include "DirectX9Manager.h" // 假设上述类定义在这个文件中 #include <mmsystem.h> #pragma comment(lib, "winmm.lib") // 窗口过程函数原型 LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM); DirectX9Manager dxManager; // WinMain: 应用程序入口点 int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR args, int nCmdShow) { WNDCLASSW wc = { 0 }; // 创建窗口类结构体实例,并初始化为0 wc.hbrBackground = (HBRUSH)COLOR_WINDOW; // 窗口背景颜色 wc.hCursor = LoadCursor(NULL, IDC_ARROW); // 窗口光标 wc.hInstance = hInst; // 应用程序实例句柄 wc.lpszClassName = L"MyWindowClass"; // 窗口类名 wc.lpfnWndProc = WindowProcedure; // 窗口过程函数 // 注册窗口类 if (!RegisterClassW(&wc)) return -1; // 创建窗口 HWND hWnd = CreateWindowW(L"MyWindowClass", L"我的窗口", WS_OVERLAPPEDWINDOW | WS_VISIBLE, 100, 100, 500, 500, NULL, NULL, NULL, NULL); //DirectX9Manager dxManager; if (!dxManager.initD3D(hWnd)) { MessageBox(hWnd, L"Failed to initialize Direct3D.", L"Error", MB_OK); return -1; } // 消息循环 MSG msg = { 0 }; float lastTime = (float)timeGetTime(); while (TRUE) { float currentTime = (float)timeGetTime(); float deltaTime = (currentTime - lastTime) * 0.001f; // convert to seconds if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); if (msg.message == WM_QUIT) break; } else { dxManager.render(deltaTime); lastTime = currentTime; } } dxManager.cleanup(); // 清理DirectX资源 return (int)msg.wParam; } LRESULT CALLBACK WindowProcedure(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_DESTROY: PostQuitMessage(0); break; case WM_LBUTTONDOWN: case WM_LBUTTONUP: case WM_MOUSEMOVE: if (dxManager.getCamera()) { return dxManager.getCamera()->HandleMessage(hWnd, message, wParam, lParam); } break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0;