目前,我们的软件渲染流程已经全部实现完毕。后续,我们会继续在此软件渲染的基础上实现几个案例,验证功能的正确性。
在本篇文章中,我们实现摄像机功能。操作方式和 UE 编辑器中的视口漫游类似:W/S/A/D/Q/E 键控制摄像机的位置平移;按住鼠标右键并移动,控制摄像头的旋转。
1. 添加 Windows 输入事件
因为现在需要响应键盘和鼠标事件,所以需要在之前的 TBasicWindow 类中添加相应的消息处理。
不过在此之前,我们先“抽象”一下逻辑。如代码清单 1.1 所示,我们定义鼠标和键盘的相关事件输入接口。
接着,如代码清单 1.2 所示,我们添加事件注册方法。
这样,我们就可以在 WindowProc 函数里调用注册好的事件处理函数。如代码清单 1.3 所示,我们添加键盘按下(WM_KEYDOWN)和弹起(WM_KEYUP)的处理,以及鼠标移动(WM_MOUSEMOVE)的处理。
2. 摄像机前向量
摄像机前向量可以通过旋转得到。但这节介绍一种新的方式,可以使用 yaw 和 pitch 角决定一个向量方向。即用 yaw 和 pitch 角定义摄像机的前向量。
yaw 是偏航角的意思,可以理解成,人左右转头;pitch 是俯仰角的意思,可以理解成,人上下点头。
如图 1 左手坐标系所示,我们设 pitch 角相对于 x-z 平面,即在 x-z 平面上“点头”。可以看到 z 坐标只和 pitch 角相关,我们可以先得到向量的 z 坐标:
如图 2 所示,设向量在 x-z 平面上的投影,与 x 轴的夹角为 yaw 角。投影的长度在图 1 中已经计算得到为 cos(pitch),所以可以得到向量的 x 和 y 坐标:
- forward.x = cos(pitch)cos(yaw)
- forward.y = cos(pitch)sin(yaw)
3. 摄像机代码实现
摄像机的前向量定位,是本章中唯一涉及到推导的地方。所以在了解如何定位之后,我们就可以开始实现摄像机相关的代码了。
如代码清单 2.1 所示,我们先大致看一下设计的摄像机类。摄像机类继承 IInputHandler 接口,后续需要实现键盘和鼠标的处理。最终想要获取的是视图矩阵(通过 GetViewMatrix 函数获取),其他变量都是为了获取视图矩阵记录的中间变量。
我们逐帧调用 GetViewMatrix 函数,更新视图矩阵。
我们首先看键盘和鼠标的输入处理。如代码清单 2.2 所示,键盘的输入,我们只先记录当前帧下按下了什么键,最后获取视图矩阵的时候再统一处理。需要记录多个按键的原因是,可能会同时按下多个键。
鼠标移动的逻辑是,只在按下鼠标右键的情况下进行状态更新。鼠标在左、右方向上的移动偏移,我们作为 yaw 角的增量;上、下方向上的偏移,我们作为 pitch 角的增量。
在 windows 窗体中,鼠标坐标系 y 轴向下增长,和图 2 中预定的方向相反。所以 y 的增量要取相反数。
同样看图 2,往 x 轴正方向移动,yaw 角会变小。所以 x 的增量也要取相反数。
获取视图矩阵的逻辑,如代码清单 2.3 所示,我们通过最新状态的 yaw 和 pitch 角,计算得到摄像机新的前向量。接着可以计算得到新的右向量和上向量。这样我们就可以更新摄像机的位置。
有了摄像机的新位置和新坐标基之后,我们可以通过 LookAtMatrix 函数得到新的视图矩阵。LookAtMatrix 函数在之前的 《空间变换》 文章中已经实现。
最后我们看一下相关变量的初始化。我们通过摄像机的初始位置和看向的位置,得到前向量。然后根据前向量,可以反推得到 yaw 和 pitch 角。
4. 测试
我们在之前绘制立方体的测试用例基础上,增加摄像机功能。如代码清单 3 所示,我们首先初始化 TCameraController 变量,然后使用 AddInputHandler 注册即可。
在这个示例中,摄像机在 z 轴 -4 的位置上,看向 z 轴正方向。
此情况下,初始化对应的 yaw 角是 90 度,pitch 角是 0 度。
示例的运行效果如视频 1 所示。可以看到,我们能够控制摄像机上下、左右、前后移动,也能够控制上下、左右方向上的旋转。。
本章的完整代码见 tag/camera_control。