前言
本次讲义是在同学们掌握Windows编程中窗口创建程序、DirectX 9.0编程中D3D初始化程序的基础上进行的,这是一次对D3D初始化程序的一次重构和类化。在此过程中,请同学们务必注意面向过程的程序语言是如何向面向对象语言转化的。
接下来,我们将创建DirectX9Manager 类,该类封装了DirectX 9的初始化、资源管理和基础设置,使得在Windows应用程序中使用DirectX进行3D图形编程变得更加简单和结构化。通过这种方式,学生可以更清晰地理解如何管理DirectX资源,以及如何设置3D渲染所需的各种参数。这种封装也提高了代码的重用性和可维护性。
类定义与成员变量
class DirectX9Manager { private: LPDIRECT3D9 d3d; // 指向Direct3D接口的指针 LPDIRECT3DDEVICE9 d3ddev; // 指向Direct3D设备的指针 public: //建立对象时,对对象进行初始化,括号中没有参数时为默认参数。 DirectX9Manager() : d3d(nullptr), d3ddev(nullptr) {} ~DirectX9Manager() { cleanup(); } bool initD3D(HWND hWnd); void cleanup(); LPDIRECT3DDEVICE9 getDevice() { return d3ddev; }//内联函数 };
代码核心如下:
public与private
public:公开,类所创建的对象可调用,其他的也可以调用。
protected:保护,子类可以调用。
private:私用,只有该类中的函数可以调用,其他的都不可以调用。
深入理解C++中public、protected及private用法
C++类成员的三种访问权限:public/protected/private
类成员变量
LPDIRECT3D9 d3d: 这是Direct3D 9接口的指针,用于创建和管理Direct3D设备。
LPDIRECT3DDEVICE9 d3ddev: 这是Direct3D设备的指针,用于执行所有的渲染操作。
构造函数与析构函数
构造函数
类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
析构函数
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
代码解析
DirectX9Manager(): 构造函数初始化指针为 nullptr,表示开始时没有创建任何Direct3D对象。
~DirectX9Manager(): 析构函数调用 cleanup() 方法,确保在对象销毁时释放所有Direct3D相关资源。
内联函数
初始化Direct3D
bool DirectX9Manager::initD3D(HWND hWnd) { // 创建Direct3D接口 d3d = Direct3DCreate9(D3D_SDK_VERSION); if (!d3d)//检测当前comment对象是否创建成功 return false; D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.Windowed = TRUE; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hWnd; // 创建Direct3D设备。 //如果d3d指向创建的设备失败,failed一般只能检测comment接口。 if (FAILED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3ddev))) { d3d->Release();//释放d3d内存 d3d = nullptr;//将d3d变成空指针 return false; } // 设置视图和投影矩阵 setupMatrices(); return true; }
成员函数
DirectX9Manager::initD3D(HWND hWnd),类名+::声明该函数为类的成员函数,然后对成员函数进行编写。
代码核心
创建Direct3D接口和设备:首先创建Direct3D接口,然后使用这个接口创建Direct3D设备。设备创建依赖于窗口句柄 (hWnd) 和一系列显示参数 (d3dpp),如是否窗口化、交换效果等。
错误处理
如果接口或设备创建失败,则释放已分配的资源并返回 false。
清理资源
void DirectX9Manager::cleanup() { if (d3ddev) { d3ddev->Release(); d3ddev = nullptr; } if (d3d) { d3d->Release(); d3d = nullptr; } } //顺序不可反,尽量和栈一样,谁先创建谁后释放。
在管理DirectX或其他系统资源的类中,正确地释放资源是防止内存泄漏和保持系统稳定的关键。这是 DirectX9Manager 类的 cleanup 方法的主要任务,具体如下:
防止资源泄漏
cleanup 确保所有通过 Direct3DCreate9 和 CreateDevice 创建的COM对象都通过调用 Release 方法来适当减少其引用计数,最终释放这些对象。使用 Release 方法来减少COM对象的引用计数。当引用计数达到零时,COM对象被销毁。
避免野指针
将指针设置为 nullptr 是一种好的编程实践,这有助于避免后续代码意外地使用已释放的对象,从而导致访问违规。即使 cleanup 被多次调用,设置指针为 nullptr 后再次调用 Release 不会产生影响,因为对 nullptr 调用 Release 是安全的。
设置相机
void DirectX9Manager::setupMatrices() { // 定义摄像机的视图矩阵 D3DXVECTOR3 vCamera(0.0f, 0.0f, -10.0f);//相机相对于世界坐标系的位置 D3DXVECTOR3 vLookat(0.0f, 0.0f, 0.0f);//相机朝向 D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f);//相机摆放 D3DXMATRIX matView;//创建一个名为matView的结构体对象,包含方法和运算符重载的 4x4 矩阵。 //计算左手视图矩阵,返回值为&matView。 D3DXMatrixLookAtLH(&matView, &vCamera, &vLookat, &vUpVec); d3ddev->SetTransform(D3DTS_VIEW, &matView); // 定义透视投影矩阵 D3DXMATRIX matProj;//透视投影矩阵 //计算左手透视投影矩阵,返回值为&matProj。 D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4, 800.0f / 600.0f, 1.0f, 100.0f); d3ddev->SetTransform(D3DTS_PROJECTION, &matProj); }
setupMatrices 方法负责设置3D渲染中的视图和投影矩阵。这些矩阵是3D图形编程中的基础,用于定义摄像机的位置和视角,以及如何将3D场景投影到2D视口上。
视图变换和投影变换矩阵的原理及推导,以及OpenGL,DirectX和Unity的对应矩阵
知识点
1) 视图矩阵 (View Matrix):视图矩阵定义了观察者(即摄像机)在世界坐标系中的位置和朝向。通过设置视图矩阵,可以确定观察者看向的点和头顶方向的向量,这对于确定哪些对象在视野中以及如何显示它们至关重要。
2) 投影矩阵 (Projection Matrix):投影矩阵定义了3D场景如何映射到屏幕坐标系(2D)。这里使用的是透视投影矩阵,它可以创建一个真实的深度感,使得对象随着距离的增加而看起来更小。
代码核心
1)D3DXMatrixLookAtLH(&matView, &vCamera, &vLookat, &vUpVec):该函数用于创建左手坐标系下的视图矩阵。它接收三个 `D3DXVECTOR3` 类型的参数:摄像机位置、目标观察点和上方向向量。
2)D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4, 800.0f / 600.0f, 1.0f, 100.0f):此函数创建透视投影矩阵,参数包括视场角度(field of view,FOV),宽高比(aspect ratio),以及近裁剪面和远裁剪面的距离。这些参数共同决定了3D场景的视觉深度和视角宽度。
– 应用矩阵:通过 `SetTransform` 方法,将计算出的视图矩阵和投影矩阵应用到设备上。`D3DTS_VIEW` 和 `D3DTS_PROJECTION` 分别指示这是设置视图矩阵和投影矩阵。
通过封装 `DirectX9Manager` 类并提供 `setupMatrices` 方法,我们不仅简化了DirectX的初始化和管理过程,还为3D图形渲染提供了必要的准备。这样的封装使得3D渲染过程更加模块化,便于理解和维护。
学习建议
1)实验不同的矩阵值:尝试修改视图矩阵和投影矩阵中的参数,观察这些变化如何影响3D场景的渲染结果。
2)理解坐标系统:深入了解DirectX使用的左手坐标系和其他图形API可能使用的坐标系之间的区别。
3)探索其他类型的投影:除了透视投影外,还可以探索正交投影等其他类型的投影,并了解它们的应用场景和视觉效果。
完整源码
DirectX9Manager类
#include <windows.h> #include <d3d9.h> #include <d3dx9.h> // Link necessary d3d9 libraries #pragma comment (lib, "d3d9.lib") #pragma comment (lib, "d3dx9.lib") class DirectX9Manager { private: LPDIRECT3D9 d3d; // the pointer to our Direct3D interface LPDIRECT3DDEVICE9 d3ddev; // the pointer to the device class public: DirectX9Manager() : d3d(nullptr), d3ddev(nullptr) {} ~DirectX9Manager() { cleanup(); } bool initD3D(HWND hWnd); void cleanup(); LPDIRECT3DDEVICE9 getDevice() { return d3ddev; } }; bool DirectX9Manager::initD3D(HWND hWnd) { d3d = Direct3DCreate9(D3D_SDK_VERSION); // create the Direct3D interface if (!d3d) return false; D3DPRESENT_PARAMETERS d3dpp; // create a struct to hold various device information ZeroMemory(&d3dpp, sizeof(d3dpp)); // clear out the struct for use d3dpp.Windowed = TRUE; // program windowed, not fullscreen d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // discard old frames d3dpp.hDeviceWindow = hWnd; // set the window to be used by Direct3D // create a device class using this information and the info from the d3dpp struct if (FAILED(d3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &d3ddev))) { d3d->Release(); d3d = nullptr; return false; } // Set up the view matrix D3DXVECTOR3 vCamera(0.0f, 0.0f, -10.0f); // camera position D3DXVECTOR3 vLookat(0.0f, 0.0f, 0.0f); // look-at position D3DXVECTOR3 vUpVec(0.0f, 1.0f, 0.0f); // up vector D3DXMATRIX matView; D3DXMatrixLookAtLH(&matView, &vCamera, &vLookat, &vUpVec); d3ddev->SetTransform(D3DTS_VIEW, &matView); // apply view matrix // Set up the projection matrix D3DXMATRIX matProj; D3DXMatrixPerspectiveFovLH(&matProj, D3DX_PI / 4, (FLOAT)800 / (FLOAT)600, 1.0f, 100.0f); d3ddev->SetTransform(D3DTS_PROJECTION, &matProj); // apply projection matrix return true; } void DirectX9Manager::cleanup() { if (d3ddev) { d3ddev->Release(); d3ddev = nullptr; } if (d3d) { d3d->Release(); d3d = nullptr } }
窗口主函数调用DirectX9Manager类的对象
#include "DirectX9Manager.h" // 假设上述类定义在这个文件中 // 窗口过程函数原型 LRESULT CALLBACK WindowProcedure(HWND, UINT, WPARAM, LPARAM); // 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}; while (TRUE) { while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); if (msg.message == WM_QUIT) return 0; } // 此处添加渲染调用 // render_frame(dxManager.getDevice()); } dxManager.cleanup(); // 清理DirectX资源 return (int) msg.wParam; } // 窗口过程函数:处理窗口消息 LRESULT CALLBACK WindowProcedure(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, msg, wp, lp); } return 0; }
引申
在该代码中,主函数WinMian传入了一个hInst的实例句柄,而后续代码中又自定义了一个窗口句柄hWnd。
将他们打印出来发现他们所指向的地址并不相同,但主函数不是只生成了一个窗口吗?
wchar_t m[100]; wsprintf(m, L"hwnd: \t%p\nhinst: \t%p", hWnd, hInst); MessageBox(NULL, m, L"标题", MB_OK);实际上,在代码运行之后,会启动一个进程,而进程中会包含一个主线程来执行mian函数。所以其实hInst指向的是进程,而hWnd指向的是该进程生成出的窗口。
一个代码启动一个进程,而进程可以调用多个线程,一般来说执行mian函数的是线程是主线程。
棒!