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矩阵。
//定义了类型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
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);
});
下面的动图中,给出了鼠标操作的效果:
