Use Camera Library
在3D渲染中,相机的本质是是模型的观察者,通过操纵相机,可以改变观察位置、观察方位,以及如何投影到屏幕上。
相机是场景的渲染通道pass的一个成员对象,可以被渲染通道的所有子对象继承。当场景中存在多个渲染通道时,每个通道可以具备自己的相机参数。
坐标变换
一个模型中往往会存在一个或者多个几何,几何的顶点位置信息是定义在该几何的局部坐标系中,但最终我们需要将它的坐标变换到屏幕空间中,才能绘制到屏幕上。这种变换其实是经历了一系列空间的转换:
- 局部空间,是几何自身的局部坐标系
- 通过Model矩阵,将顶点位置信息从局部空间转换到世界空间
- 通过View矩阵,从世界空间转换到相机的观察空间,或者视觉空间
- 通过Projection矩阵,从观察空间转换到屏幕空间
- 从原始的几何文件中,我们可以获取到这些几何的Model矩阵(也可以看作transform矩阵),Model矩阵通过对几何进行平移、缩放和旋转,将它的顶点位置转换为世界坐标系中。
- 接下来将世界坐标变换为观察空间坐标,使得每个顶点都是从摄像机或者是观察者的角度进行观察。摄像机的View矩阵也是由一系列的平移、缩放和旋转变换组合而成。
- 坐标转换到观察空间后,我们需要将其投影到屏幕空间中,其实可以分为两个小步,第一步是通过Projection矩阵从观察空间坐标转换到裁剪坐标中,坐标值会被处理至标准化设备坐标的-1.0到1.0范围内,因此有些顶点不在该范围内的话,会被裁剪掉,不会出现在屏幕中。第二步则是将-1.0到1.0的范围变换到屏幕的实际坐标范围内。
Projection矩阵可以分为两种不同的形式,分别定义了不同的观察箱:正交投影矩阵和透视投影矩阵。在我们的库中目前只提供了正交投影矩阵。
- 最终得到的坐标会被送到光栅器中,转换为fragments。
我们将上述过程用矩阵公式描述:
当需要调整模型的观察角度、方位和投影时,需要改变模型所在渲染通道中camera的viewMatrix与projectionMatrix,在我们库中,将这些调整都封装到相机的各种操纵方式中,开发者既可以直接修改相机的viewMatrix与projectionMatrix,也可以直接调用操纵相机的函数接口。
定义相机
相机具备了以下几个属性:
View矩阵
View矩阵可以把世界空间的坐标变换成相对于摄像机的观察坐标,当我们要定义一个相机的View矩阵时,需要知道四个参数:
- 相机在世界空间中的位置eye,也就是一个从世界空间的原点指向相机位置的向量
- 观察方向dir,它其实是指向了从相机到渲染场景中心位置的向量的相反方向
- 一个指向相机右侧的向量right,它代表了相机观察空间的X轴的正方向
- 一个指向相机上方的向量up,代表了相机观察空间的Y轴的正方向
使用这些向量,我们就可以创建一个LookAt矩阵,也就是一个看着给定目标的观察矩阵View Matrix。
Projection矩阵
Projection矩阵的作用是把观察空间坐标转化为标准化设备坐标,这里我们只介绍正交投影矩阵,它类似于一个立方体一样的箱子,定义了一个裁剪空间,空间之外的顶点都被裁剪掉。与view矩阵一样,Projection矩阵也需要指定几个参数,在这几个参数作用下形成了Projection矩阵。
- orthoSize:裁剪范围的宽度和高度,即视野在水平方向和垂直方向的范围
- orthoMinMax:裁剪范围的近面和远面
- aspect:裁剪范围的宽度/高度的比值。
使用这些参数,我们就可以创建一个Projection矩阵。我们用left, right, bottom和top表示视野在水平方向和垂直方向的范围,near和far表示深度方向的范围。
旋转中心orbitPoint
当旋转相机时,相机会围绕着自己的orbitPoint进行旋转。您可以在自己的程序中,指定相机的旋转中心,往往这个中心需要放在物体的中心,或者您的鼠标击打到的位置。
修改View矩阵
当您已经确定了相机的位置eye,观察方向dir,指向右侧的向量right以及指向上方的向量up,我们就可以用下面这个接口直接计算出View矩阵。
//定义了结构体viewSpec,包含了相机的四个向量
struct ViewSpec {
Vector3fPtr right;
Vector3fPtr up;
Vector3fPtr dir;
Vector3fPtr eye;
};
// 初始化一个view矩阵
auto viewMatrix = _mat4();
// 利用相机的四个向量,修改了view矩阵
cil::setViewSpec(matrix, viewSpec);
同理,当您已经确定了View矩阵,也可以得到相机的四个向量
cil::getViewSpec(viewMatrix);
修改Projection矩阵
我们提供了以下接口,帮助修改Projection矩阵
setOrthoSize(projectionMatrix, size):将新的裁剪范围的宽度、高度作为输入,修改projection矩阵。setOrthoMinMax(projectionMatrix, minMax):将新的裁剪范围的近远面作为输入,修改projection矩阵。setAspect(projectionMatrix, aspect):裁剪范围的高度不变,按照新的aspect值,更新宽度,修改projection矩阵。
同样地,您也可以通过get函数,获取Projection矩阵的这些参数:
getOrthoSize(projectionMatrix)getOrthoMinMax(projectionMatrix)getAspect(projectionMatrix)
操纵相机
下面介绍了您可以直接在业务层中使用的几个函数,便捷操纵相机平移、旋转和缩放。
home
home接口是用来自动调整视图矩阵和投影矩阵,使得整个场景模型在设备窗口中完全可见,并且从(1,1,1)方向观察。
// 定义了背景,其大小等于窗口的像素大小。
auto pixelSize = widget->pixelSize();
auto background = _simpleBackground(_ivec2(pixelSize),
_clr4(0.9, 0.8, 0.7, 1.0));
...
// 定义了一个相机对象
auto camera = _camera();
auto pass = _pass(node, background, camera);
auto scene = _scene({ pass });
// 定义了场景中完整模型的几何包围盒
auto boundingBox = _bbox(-38.21760177612305, 43.67150115966797, -25.4783992767334, 25.4783992767334, 0, 40.12839889526367);
// 使用home函数,自动按照几何包围盒、背景大小调整当前的相机
home(camera, *boundingBox, *background->size);
fit
fit函数可以自动调整投影矩阵,使得在当前观察场景不变的情况下,整个模型场景可见。
// 自适应窗口,以看到整个场景
fit(camera, *boundingBox);
look
look函数可以用来自动调整相机位置,使相机从某个指定的轴向去观察场景。
// 从z方向观察场景
look(camera, *boundingBox, "z");
navigate
navigate是指通过操作鼠标,或者3D鼠标,触摸屏等实现对相机的操纵,包括Zooming, Panning, Rotating和Orbiting这几类。这里要求您安装manipulator库,并且在程序中添加头文件cilmanipulator.h。
在manipulator库中定义了类NavigationManipulator。它包含了几种鼠标操作函数,通过这些操作,可以操控相机的位置、朝向和远近。
auto navigation = std::make_shared<NavigationManipulator>(camera, windowSize, boundingBox);
| Navigating函数 | 描述 |
|---|---|
| mouseDown(coord, button, modifier) | 记录下鼠标被按下时光标的位置和按键类型,button可取值Left,Right,Middle, modifier可取值Shift,Ctrl,Alt |
| mouseMove(coord, button, modifier) | 当button值为Left,执行rotating;当button值为Middle,执行panning;当button值为Right时,执行rolling |
| mouseUp(coord, button, modifier) | 完成操作 |
| mouseWheel(coord, modifier, up) | 执行zooming |
您可以为定义回调函数。程序中每次执行了相机操纵(orbiting, panning等)之后,会执行该回调函数.
navigation->bind("triggered", [&](MouseAction* action) -> void {
renderer->render(scene);
widget->update();
});
下面给出一个案例,应用了上面所有的Navigation函数,您执行这段程序后,就可以在窗口中使用鼠标自由操纵相机了。
首先您在视图区中创建一个渲染单元,首先您可以通过来自于geometry库中的_teapot接口,创建一个我们提供的几何teapot。然后使用材料接口_phoneMaterial,创建一个光照材料,与teapot共同组成了这个案例的渲染单元primitive。
auto geometry = _teapot();
auto material = _phong(_clr3(0.5, 0.6, 0.8));
auto primitive = _primitive(geometry, material);
然后,我们组成渲染场景,详细说就是构造node,camera, pass和scene。我们使用home函数,自动将摄像机调整到一个合适的视角。
auto node = _node({ primitive });
auto camera = _camera();
auto pass = _pass(node, background, camera);
auto scene = _scene({ pass });
auto boundingBox = geometry->boundingBox;
*camera->orbitPoint = boundingBox->center();
home(camera, *boundingBox, *background->size);
在OpenGLWidget类中,我们定义了一系列鼠标事件MouseEvent,它可以用来保存在窗口中使用鼠标的一系列行为,那么我们可以利用这些鼠标事件,调用navigation提供的接口。
在程序的最开始,我们就定义了一个OpenGLWidget类对象widget,OpenGLWidget类的定义在/common/glfw/openglwidget.h文件中,所以您需要在程序开头引入cilglfw.h头文件。
widget->onMouseDown = [&](MouseEvent* event) -> void {
auto button = getButton(event->button);
auto modifier = getModifier(event->modifier);
navigation->mouseDown(ivec2(event->x, event->y), button, modifier);
};
widget->onMouseDrag = [&](MouseEvent* event) -> void {
auto button = getButton(event->button);
auto modifier = getModifier(event->modifier);
navigation->mouseMove(ivec2(event->x, event->y), button, modifier);
};
widget->onMouseUp = [&](MouseEvent* event) -> void {
auto button = getButton(event->button);
auto modifier = getModifier(event->modifier);
navigation->mouseUp(ivec2(event->x, event->y), button, modifier);
};
widget->onWheel = [&](WheelEvent* event) -> void {
auto modifier = getModifier(event->modifier);
navigation->mouseWheel(ivec2(event->x, event->y), modifier, event->up);
};
下面的动图中,给出了鼠标操作的效果:
