Skip to main content

Use Camera Library

在3D渲染中,相机的本质是是模型的观察者,通过操纵相机,可以改变观察位置、观察方位,以及如何投影到屏幕上。

相机是场景的渲染通道pass的一个成员对象,可以被渲染通道的所有子对象继承。当场景中存在多个渲染通道时,每个通道可以具备自己的相机参数。

坐标变换

一个模型中往往会存在一个或者多个几何,几何的顶点位置信息是定义在该几何的局部坐标系中,但最终我们需要将它的坐标变换到屏幕空间中,才能绘制到屏幕上。这种变换其实是经历了一系列空间的转换:

  • 局部空间,是几何自身的局部坐标系
  • 通过Model矩阵,将顶点位置信息从局部空间转换到世界空间
  • 通过View矩阵,从世界空间转换到相机的观察空间,或者视觉空间
  • 通过Projection矩阵,从观察空间转换到屏幕空间
  1. 从原始的几何文件中,我们可以获取到这些几何的Model矩阵(也可以看作transform矩阵),Model矩阵通过对几何进行平移、缩放和旋转,将它的顶点位置转换为世界坐标系中。
  2. 接下来将世界坐标变换为观察空间坐标,使得每个顶点都是从摄像机或者是观察者的角度进行观察。摄像机的View矩阵也是由一系列的平移、缩放和旋转变换组合而成。
  3. 坐标转换到观察空间后,我们需要将其投影到屏幕空间中,其实可以分为两个小步,第一步是通过Projection矩阵从观察空间坐标转换到裁剪坐标中,坐标值会被处理至标准化设备坐标的-1.0到1.0范围内,因此有些顶点不在该范围内的话,会被裁剪掉,不会出现在屏幕中。第二步则是将-1.0到1.0的范围变换到屏幕的实际坐标范围内。
tip

Projection矩阵可以分为两种不同的形式,分别定义了不同的观察箱:正交投影矩阵和透视投影矩阵。在我们的库中目前只提供了正交投影矩阵。

  1. 最终得到的坐标会被送到光栅器中,转换为fragments。

我们将上述过程用矩阵公式描述:

Vclip=MprojectionMviewMmodelVlocalV_{clip} = M_{projection} * M_{view} * M_{model} * V_{local}

当需要调整模型的观察角度、方位和投影时,需要改变模型所在渲染通道中camera的viewMatrix与projectionMatrix,在我们库中,将这些调整都封装到相机的各种操纵方式中,开发者既可以直接修改相机的viewMatrix与projectionMatrix,也可以直接调用操纵相机的函数接口。

定义相机

相机具备了以下几个属性:

View矩阵

View矩阵可以把世界空间的坐标变换成相对于摄像机的观察坐标,当我们要定义一个相机的View矩阵时,需要知道四个参数:

  • 相机在世界空间中的位置eye,也就是一个从世界空间的原点指向相机位置的向量
  • 观察方向dir,它其实是指向了从相机到渲染场景中心位置的向量的相反方向
  • 一个指向相机右侧的向量right,它代表了相机观察空间的X轴的正方向
  • 一个指向相机上方的向量up,代表了相机观察空间的Y轴的正方向

使用这些向量,我们就可以创建一个LookAt矩阵,也就是一个看着给定目标的观察矩阵View Matrix。

LookAt=(RxRyRz0UxUyUz0DxDyDz00001)(100Px010Py001Pz0001)LookAt = \begin{pmatrix} R_x & R_y & R_z & 0 \\ U_x & U_y & U_z & 0\\ D_x & D_y & D_z & 0\\ 0 & 0 & 0 & 1\\ \end{pmatrix} * \begin{pmatrix} 1 & 0 & 0 & -P_x \\ 0 & 1 & 0 & -P_y\\ 0 & 0 & 1 & -P_z\\ 0 & 0 & 0 & 1\\ \end{pmatrix}

Projection矩阵

Projection矩阵的作用是把观察空间坐标转化为标准化设备坐标,这里我们只介绍正交投影矩阵,它类似于一个立方体一样的箱子,定义了一个裁剪空间,空间之外的顶点都被裁剪掉。与view矩阵一样,Projection矩阵也需要指定几个参数,在这几个参数作用下形成了Projection矩阵。

  • orthoSize:裁剪范围的宽度和高度,即视野在水平方向和垂直方向的范围
  • orthoMinMax:裁剪范围的近面和远面
  • aspect:裁剪范围的宽度/高度的比值。

使用这些参数,我们就可以创建一个Projection矩阵。我们用left, right, bottom和top表示视野在水平方向和垂直方向的范围,near和far表示深度方向的范围。

Projection=(2rightleft00right+leftrightleft02topbottom0top+bottomtopbottom002farnearfar+nearfarnear0001)Projection = \begin{pmatrix} \frac{2}{right-left} & 0 & 0 & -\frac{right+left}{right-left} \\ 0 & \frac{2}{top-bottom} & 0 & -\frac{top+bottom}{top-bottom}\\ 0 & 0 & -\frac{2}{far-near} & -\frac{far+near}{far-near}\\ 0 & 0 & 0 & 1\\ \end{pmatrix}

旋转中心orbitPoint

当旋转相机时,相机会围绕着自己的orbitPoint进行旋转。您可以在自己的程序中,指定相机的旋转中心,往往这个中心需要放在物体的中心,或者您的鼠标击打到的位置。

修改View矩阵

当您已经确定了相机的位置eye,观察方向dir,指向右侧的向量right以及指向上方的向量up,我们就可以用下面这个接口直接计算出View矩阵。

//定义了类型JViewSpec,包含了相机的四个向量
interface JViewSpec {
eye: Vector3;
dir: Vector3;
right: Vector3;
up: Vector3;
}

// 初始化一个view矩阵
const viewMatrix = mat4();
// 利用相机的四个向量,修改了view矩阵
setViewSpec(viewMatrix, viewSpec);

同理,当您已经确定了View矩阵,也可以得到相机的四个向量

const viewSpec = 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)方向观察。

import { home } from 'ore.camera';

// 定义了背景,其大小等于窗口的像素大小。
const background = Background({
size: ivec2(canvas.width, canvas.height),
colorMask: clr4(0.9, 0.8, 0.7, 1.0),
depthMask: float(1.0)
});
// 定义了一个相机对象
const camera = Camera();
const pass = Pass({ background, camera, node });
const scene: JScene = {
name: 'scene',
passes: [pass]
};

// 定义了场景中完整模型的几何包围盒
const boundingBox = bbox(-38.21760177612305, 43.67150115966797, -25.4783992767334,25.4783992767334, 0, 40.12839889526367);
// 使用home函数,自动按照几何包围盒、背景大小调整当前的相机
home(camera, boundingBox, background.size);

fit

fit函数可以自动调整投影矩阵,使得在当前观察场景不变的情况下,整个模型场景可见。

import { fit } from 'ore.camera';

// 自适应窗口,以看到整个场景
fit(camera, boundingBox);

look

look函数可以用来自动调整相机位置,使相机从某个指定的轴向去观察场景。

import { look } from 'ore.camera';

// 从z方向观察场景
look(camera, boundingBox, 'z');

navigate是指通过操作鼠标、3D鼠标或者触摸屏等实现对相机的操纵,包括zooming, panning, rotating和orbiting这几类。

这里要求您安装ore.manipulator库,在manipulator库中定义了类Navigation,它包含了几种鼠标操作函数,通过这些操作,可以操控相机的位置、朝向和远近。

import { Navigation } from 'ore.manipulator';

const navigation = new Navigation(canvas, camera, windowSize, boundingBox);

下面给出一个案例,应用了上面所有的Navigation函数,您执行这段程序后,就可以在窗口中使用鼠标自由操纵相机了。

首先您在视图区中创建一个渲染单元,首先您可以创建一个我们提供的几何teapot。然后使用材料phong_color,创建一个光照材料,与teapot共同组成了这个案例的渲染节点node

const geometry = await loadStanford('teapot');
const color = clr3(0.5, 0.6, 0.8);
const material = phong_color(color);
const node: JNode = {
mesh: {
primitives: [{ geometry, material }]
}
};

然后,我们组成渲染场景,详细说就是构造camera, pass和scene。我们使用home函数,自动将摄像机调整到一个合适的视角。

const camera = Camera();
const pass = Pass({
background: background,
camera: camera,
node: {
children: [node],
}
});
const scene: JScene = {
name: 'scene',
passes: [pass],
};

const boundingBox = geometry.boundingBox;
camera.orbitPoint = boundingBox.center();
home(camera, boundingBox, background.size);

最后,创建一个Navigation,并绑定updated事件,来实现当照相机发生变化的时候重绘整个场景。

const windowSize = ivec2(canvas.clientWidth, canvas.clientHeight);
const navigation = new Navigation(canvas, camera, windowSize, boundingBox);
navigation.bind('updated', (): void => {
renderer.render(scene);
});

下面的动图中,给出了鼠标操作的效果:

navigation