Web编程技术营地
研究、演示、创新

Camera

撰写时间:2023-09-13

修订时间:2026-04-27

我们之前说过,glMatrixlookAt函数很不好用,因为对眼睛所在的位置与所望向的终点位置所构成的直线会导致场景视图产生何种视图变换,在数学运算上是很方便,但在人看来,很不直观,无法提前预判其结果。

WebGL应用中只要求有视图矩阵及其他相应矩阵,因此相机的实现是WebGL应用的一个奢侈品。但由于我们对视图矩阵感觉很抽象,对相机却很熟悉,因此,若能将视图矩阵的变换抽象为相机的取景,则我们的感觉会轻松许多。

我们的思路是,先将相机放在物体的正前方,然后围绕着世界坐标系原点,相机在水平方向上旋转多少度、在竖直方向上旋转多少度来取景。这就非常好理解了。

系统架构中的设计理念

Camera类很简单,因为其主要职责仅在于维护自身的实例变量viewMatrix就行了,它代表了相机视图。

在同时有多个对象协作的系统架构中,Camera是一个完全被动的对象。

第一,Camera对外公开了zoom, pan, rotate3接口方法,这样,当用户在应用程序界面中通过键盘、鼠标进行视图操作时,负责监听键盘、鼠标事件的Controls类将调用Camera实例的这些方法,它们将直接改变Camerapositionazimuthelevation等实例变量,从而为更新实例变量viewMatrix作好准备。

第二,Controls类直接调用ViewportManagerdoOnCameraViewChanged方法,委派各个Viewport响应相机视图改变事件。

需注意的是,这里也可使用触发事件再监听事件的机制,但由于ViewportManager已持有需响应此类事件的各个Viewport实例,可以直接责令它们立即响应事件,因此反倒可以省去触发事件、监听事件的额外步骤,应用程序效能得以提升不少。尽管如此,其方法名doOnCameraViewChanged仍清晰地表达了这种理念。

第三,在各个ViewportapplyTransforms方法中,将调用CameragetViewMatrix接口方法来获取Camera视图矩阵,以投喂顶点着色器中的uViewMatrix变量。

第四,而在getViewMatrix中,Camera先调用自身方法updateViewMatrix,将自身的各个实例变量整合为viewMatrix的状态,然后再对外返回viewMatrix

因此,系统架构中各个类的接口很重要,当这些接口一确定下来,各个类就可以在生产线上无缝协作了。

并且,除对外公开的接口之外,各个类均很好地隐藏它们自己的各种内部状态,在以后的升级与完善中,这些内部状态的修改,不会影响整个系统架构。

下面,我们依此理念来设计Camera类。

构造方法

export const CAMERA_TYPE = { USER_VIEW: 0, FRONT_VIEW: 1, BACK_VIEW: 2, LEFT_VIEW: 3, RIGHT_VIEW: 4, TOP_VIEW: 5, BOTTOM_VIEW: 6 }; export class Camera { fov = 0; initialPosition = vec3.create(); // property that can be restored later position = vec3.create(); azimuth = 0.0; // horizontal elevation = 0.0; // vertical type; viewMatrix = mat4.create(); constructor(fov = 30, position = [0, 0, 10], type) { this.setFov(fov); this.initPosition(position); this.setAzimuth(0); this.setElevation(0); this.setType(type); } setFov(fov) { this.fov = fov; } initPosition(position) { vec3.copy(this.initialPosition, position); this.setPosition(position); } setPosition(position) { vec3.copy(this.position, position); } setAzimuth(degree) { this.azimuth = degree; } setElevation(degree) { this.elevation = degree; } setType(type) { this.type = type; } ... }

类属性fov表示视域,position表示相机在世界坐标系Z轴上的位置,azimuth表示水平围绕Y轴的旋转角度,elevation表示围绕X轴的旋转角度,type表示相机类型。它们对外都有一个对应的用以设置其值的接口方法。

initialPosition表示相机初始的位置。引入该属性的原因在于,position总在不断地变化,而用户如果想恢复相机最初的初始位置值时,则可使用initialPosition的值。

视图变换方法

zoom(dz) { this.position[2] += dz; } pan(dx, dy) { this.position[0] += dx; this.position[1] += dy; } rotate(dx, dy) { this.azimuth += dx; this.elevation += dy; }

这一组接口方法分别用以实现相机的推拉焦距、平移及旋转。它们的参数都是偏移值,在运行时,这些参数的值都来自于Controls每次捕获到的用户通过键盘、鼠标来输入的偏移值。而在方法体中,均简单地将这些偏移值累加到相应的类属性值中。

注意rotate方法,其参数并非表示相机围绕X轴Y轴来旋转,而是分别表示相机在水平方向上旋转(即围绕Y轴旋转)及在竖直方向上旋转(即围绕X轴旋转)的偏移量,这样的设计,大脑反应最快。

对外提供相机视图矩阵

上面的方法均只是简单地设置了类的各个属性值,它们都属于内部状态。而要真实地反映出相机的视图,我们仍需根据这些属性值计算出视图矩阵,并返回该矩阵。getViewMatrix方法用于此目的。

getViewMatrix() { this.updateViewMatrix(); const matrix = mat4.create(); mat4.invert(matrix, this.viewMatrix); return matrix; } updateViewMatrix() { mat4.identity(this.viewMatrix); mat4.rotateY(this.viewMatrix, this.viewMatrix, glMatrix.toRadian(this.azimuth)); mat4.rotateX(this.viewMatrix, this.viewMatrix, glMatrix.toRadian(-this.elevation)); mat4.translate(this.viewMatrix, this.viewMatrix, this.position); }

在返回相机视图矩阵时,有一点需注意,Camera类所存储的矩阵与客户端所期待的矩阵正好是相反的。例如,若相机右移5个单位,其效果就是所拍摄的整个场景左移5个单位,也即,场景的视图矩阵左移5个单位。

而在每次渲染中,我们都需要向着色器传输场景视图矩阵的值,这就意味着我们需要翻转相机的视图矩阵后,再返回该矩阵

getViewMatrix方法中,先调用updateViewMatrix方法计算出相机的视图矩阵,然后调用mat4invert方法来翻转矩阵,最后返回该矩阵

其他功能

Camera类还有一个copyFrom方法,用以将一个Camera对象的内部状态全部复制为自己的状态。

copyFrom(src) { this.fov = src.fov; this.initialPosition = src.initialPosition; this.position = src.position; this.azimuth = src.azimuth; this.elevation = src.elevation; this.type = src.type; }

当我们在视口中通过快捷键切换为不同的预定义相机视图时,我们先将预定义的相机视图复制到当前视口的相机中,这样既使用了预定义的相机视图,且用户随后的对视图的一系列操作又不至于影响到预定义的相机视图。

打印Camera类

运行Camera-info.html

网页左上角打印出Camera类的信息,及其可用的键盘快捷键及鼠标操作。

这个例子还演示了如何在一个视口中精细地操控相机:

第一,相机初始化工作可在appdoInInitViewports方法中完成,因为其回调函数的参数中自动传入了cameraManagerviewportManager这两个实例。

第二,代码:

将观察点设置为世界坐标系原点前方10个单位的位置。

第三,代码:

将帮我们自动创建了一个视口,也自动创建了视口中的相机,且视口的相机视图初始化为用户视图(相机水平旋转20o,垂直旋转30o)。

第四,代码:

从当前视口取出其相机。

第五,代码:

取出预定义的前视图,将其配置复制到当前相机中。我们准备在前视图的基础上进行修改。

第六,代码:

相机往上抬一点,且围绕世界坐标系原点水平旋转25o,垂直旋转10o

有此相机在手,我们还怕拍不好照吗?

参考资源

  1. WebGL 2.0 Specification
  2. 详解投影变换