这里回顾前三节知识。
今天我们来聊一聊摄影机(camera)以及透视矩阵(perspective)。
摄影机
通过第一节的知识,我们已经知道了如何对模型矩阵进行变换,从而可以使物体可以做各种运动。但更多的时候,我们希望的是自己能够控制视角去观察物体的变化。引入摄影机的概念,便帮助我们解决了这个需求。
摄影机也叫观察空间(View Space)。这个概念不用过多解释,在Processing的3D空间中就相当于我们的眼睛。观察矩阵把所有的世界坐标转换到观察坐标,这些新的坐标是相对摄影机的位置和方向的。
定义一个摄影机,我们需要摄影机在世界空间中的:
- 位置
- 观察方向
- 一个指向它的右侧的向量(右轴)
- 一个指向它上方的向量(上轴)

step 1 摄像机位置
真实世界的摄影机可以前后左右上下满世界乱跑。摄像机位置简单来说就是世界空间中代表摄像机位置的向量。Processing中摄影机的默认位置为屏幕中央并向我们(屏幕外)退后少许,这样才能拍到屏幕中的网格对象嘛。(多边形组成的模型对象称为网格,有单个网格组成的模型,也有几个网格合并组成的模型)
我们来看看定义:
cx =width / 2.0;
cy =height / 2.0;
// PI*30.0 / 180.0是度转弧的一种写法,就是tan(30°)
cz =(height / 2.0) / tan(PI*30.0 / 180.0);
PVector cameraPos = new PVector(cx, cy, cz);
step 2 摄影机方向
我们的视线需要一个方向,摄影机也需要一个方向。Processing中摄影机的方向指向屏幕中央。
PVector cameraTarget = new PVector(width/2, height/2, 0);
// 附带的,这里我们会得到一个方向向量(这里只需要了解)
PVector cameraDirection = PVector.sub(cameraPos, cameraTarget);
cameraDirection.normalize();
step 3 摄影机上向量
人为定义一个上向量(Up Vector)。Processing默认为:
PVector up = new PVector(0, 1, 0);
上向量是用来干吗的呢?有了上向量,我们可以求出摄影机的右轴和上轴,从而创建一个LookAt矩阵帮助我们实现摄影机的功能。
这里说说过程,你只需了解就好,因为他们都由Processing帮助我们计算完成。 我们把上向量和第二步得到的摄影机方向向量叉乘。两个向量叉乘的结果就是同时垂直于两向量的向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量的顺序就会得到相反的指向x轴负方向的向量:
PVector cameraRight = up.cross(cameraDirection);
cameraRight.normalize();
实现摄影机
Processing帮我们把这一摊子事情都封装成了一个小小的函数: camera()。 我们只需要向camera()函数中填入摄影机的位置,方向及上向量,就可以一切ok。
// 设置摄影机
PVector cameraPos = new PVector(width/2.0, height/2.0, (height/2.0) / tan(PI*30.0 / 180.0));
PVector cameraTarget = new PVector(width/2.0, height/2.0, 0.0);
PVector up = new PVector(0, 1, 0);
camera(cameraPos.x, cameraPos.y, cameraPos.z,
cameraTarget.x, cameraTarget.y, cameraTarget.z,
up.x, up.y, up.z);
控制摄影机移动
配置好了摄影机,我们当然希望能操作摄影机~。 这十分简单,我们分别控制摄影机的位置cameraPos, 和摄影机的目标cameraTarget即可。
void do_movement() {
float cameraSpeed = 5.0;
// 移动摄影机(同时移动目标)
if (keyPressed) {
// 前后
if (key == 'w') {
cameraPos.z -= cameraSpeed;
cameraTarget.z -= cameraSpeed;
}
if (key == 's') {
cameraPos.z += cameraSpeed;
cameraTarget.z += cameraSpeed;
}
// 左右
if (key == 'a') {
cameraPos.x -= cameraSpeed;
cameraTarget.x -= cameraSpeed;
}
if (key == 'd') {
cameraPos.x += cameraSpeed;
cameraTarget.x += cameraSpeed;
}
}
// 转动摄影机视角(即移动摄影机目标)
// 偏航
cameraTarget.x += mouseX - pmouseX;
// 俯仰
cameraTarget.y += mouseY - pmouseY;
}
注意移动摄影机时,要连带移动摄影机目标,否则我们的摄影机只会盯住目标不放= =!。 很多同学不知道如何让物体自旋。有了摄影机这个观察者视角,你想让物体怎么折腾都可以了。 这里有完整代码(这里先简单的改变摄像机):
int w = 800;
int h = 800;
// 定义顶点
PVector[] ver;
PVector[] face;
PVector[][] uv;
PVector[] cubesPos;
PImage tex;
PVector cameraPos, cameraTarget, up;
PVector camTranslate, camRotate;
void settings() {
//fullScreen();
size(w, h, P3D);
}
void setup() {
// 设置顶点
ver = new PVector[8];
ver[0] = new PVector(-0.5, -0.5, 0.5); // 顶点1
ver[1] = new PVector(-0.5, -0.5, -0.5); // 顶点2
ver[2] = new PVector( 0.5, -0.5, -0.5); // ...
ver[3] = new PVector( 0.5, -0.5, 0.5);
ver[4] = new PVector(-0.5, 0.5, 0.5);
ver[5] = new PVector(-0.5, 0.5, -0.5);
ver[6] = new PVector( 0.5, 0.5, -0.5);
ver[7] = new PVector( 0.5, 0.5, 0.5);
// 设置顶点索引
face = new PVector[12];
// top
face[0] = new PVector(0, 1, 2);
face[1] = new PVector(0, 2, 3);
// front
face[2] = new PVector(0, 3, 7);
face[3] = new PVector(0, 7, 4);
// back
face[4] = new PVector(1, 2, 6);
face[5] = new PVector(1, 6, 5);
// right
face[6] = new PVector(3, 2, 7);
face[7] = new PVector(2, 6, 7);
// left
face[8] = new PVector(0, 4, 5);
face[9] = new PVector(1, 5, 0);
// bottom
face[10] = new PVector(4, 5, 7);
face[11] = new PVector(5, 6, 7);
// 设置UV
// 目前有一点苦力活需要干,但很快,将会有东西拯救我们。
uv = new PVector[12][3]; // 12个面,每个面3个顶点,为每个顶点描述UV
float max = 2.0;
// top
uv[0][0] = new PVector(0, max); // face0
uv[0][1] = new PVector(0, 0);
uv[0][2] = new PVector(max, 0);
uv[1][0] = new PVector(0, max); // face1
uv[1][1] = new PVector(max, 0);
uv[1][2] = new PVector(max, max);
// front
uv[2][0] = new PVector(0, 0); // face2
uv[2][1] = new PVector(max, 0);
uv[2][2] = new PVector(max, max);
uv[3][0] = new PVector(0, 0); // face3
uv[3][1] = new PVector(max, max);
uv[3][2] = new PVector(0, max);
// back
uv[4][0] = new PVector(0, 0); // face4
uv[4][1] = new PVector(max, 0);
uv[4][2] = new PVector(max, max);
uv[5][0] = new PVector(0, 0); // face5
uv[5][1] = new PVector(max, max);
uv[5][2] = new PVector(0, max);
// right
uv[6][0] = new PVector(0, 0); // face6
uv[6][1] = new PVector(max, 0);
uv[6][2] = new PVector(0, max);
uv[7][0] = new PVector(max, 0); // face7
uv[7][1] = new PVector(max, max);
uv[7][2] = new PVector(0, max);
// left
uv[8][0] = new PVector(0, 0); // face8
uv[8][1] = new PVector(0, max);
uv[8][2] = new PVector(max, max);
uv[9][0] = new PVector(max, 0); // face9
uv[9][1] = new PVector(max, max);
uv[9][2] = new PVector(0, 0);
// bottom
uv[10][0] = new PVector(0, max); // face10
uv[10][1] = new PVector(0, 0);
uv[10][2] = new PVector(max, max);
uv[11][0] = new PVector(0, 0); // face11
uv[11][1] = new PVector(max, 0);
uv[11][2] = new PVector(max, max);
// 设置cubes的位置坐标
cubesPos = new PVector[3];
cubesPos[0] = new PVector( 0, 0, 0);
cubesPos[1] = new PVector( 500, 0, 0);
cubesPos[2] = new PVector( 0, 0, 400);
// 设置图形模式
noStroke();
// 载入贴图
tex = loadImage("t3.jpg");
tex.resize(int(256/max), 0);
// 设置纹理属性
textureMode(NORMAL);
textureWrap(REPEAT);
// 设置摄影机
cameraPos = new PVector(width/2.0, height/2.0, (height/2.0) / tan(PI*30.0/180.0));
cameraTarget = new PVector(width/2.0, 0.0, 0.0);
up = new PVector(0, 1, 0);
camTranslate = new PVector(0, 0, 0);
camRotate = new PVector(0, 0, 0);
//noCursor();
}
float angle1 = 0;
float angle2 = 0;
void draw() {
// 清楚缓冲区
background(0);
// 事件处理
do_movement();
camera(cameraPos.x, cameraPos.y, cameraPos.z,
cameraTarget.x, cameraTarget.y, cameraTarget.z,
up.x, up.y, up.z);
// 绘制图形
for (int n = 0; n < cubesPos.length; n++) {
// 模型矩阵变换
translate(cubesPos[n].x, cubesPos[n].y, cubesPos[n].z);
pushMatrix();
// 单个模型矩阵变换
translate(width/2, height/2, -100.0);
scale(200);
// 绘制单个网格物体
//fill(255, 127, 39);
for (int i = 0; i < face.length; i++) {
beginShape();
texture(tex);
// x , y , z , u , v
vertex(ver[int(face[i].x)].x, ver[int(face[i].x)].y, ver[int(face[i].x)].z, uv[i][0].x, uv[i][0].y);
vertex(ver[int(face[i].y)].x, ver[int(face[i].y)].y, ver[int(face[i].y)].z, uv[i][1].x, uv[i][1].y);
vertex(ver[int(face[i].z)].x, ver[int(face[i].z)].y, ver[int(face[i].z)].z, uv[i][2].x, uv[i][2].y);
endShape(TRIANGLES);
}
popMatrix();
}
}
void do_movement() {
float cameraSpeed = 5.0;
// 移动摄影机(同时移动目标)
if (keyPressed) {
// 前后
if (key == 'w') {
cameraPos.z -= cameraSpeed;
cameraTarget.z -= cameraSpeed;
}
if (key == 's') {
cameraPos.z += cameraSpeed;
cameraTarget.z += cameraSpeed;
}
// 左右
if (key == 'a') {
cameraPos.x -= cameraSpeed;
cameraTarget.x -= cameraSpeed;
}
if (key == 'd') {
cameraPos.x += cameraSpeed;
cameraTarget.x += cameraSpeed;
}
}
// 转动摄影机视角
// 偏航
cameraTarget.x += mouseX - pmouseX;
// 俯仰
cameraTarget.y += mouseY - pmouseY;
}
透视
现实生活中离你越远的东西看起来越小,铁轨公路什么的就是最好的例子。这种视觉效果我们称之为 透视(Perspective)。我们将使用 透视投影(Perspective Projection)来模仿这样的效果,它是使用透视矩阵来完成的。这个矩阵修改了每个顶点的w值,从而使得离观察者越远的顶点坐标w分量越大。
out = (x/w, y/w, z/w);
在Processing中创建投影矩阵很简单:perspective() 函数。我们来看看具体的方法,投影矩阵要放在摄影机矩阵之后实现:
// 投影矩阵(透视)
float fov = PI/3.0; // 视野(Field of View)
float aspect = float(width)/float(height); // 画幅比例
float zNear = cameraPos.z/10.0; // 近焦平面
float zFar = cameraPos.z*10.0; // 远焦平面
perspective(fov, aspect, zNear, zFar);
fov也就是视野(Field of View),相信玩过3D类游戏的朋友应该都清楚。通常设定在45,60,75,90这几个档位。你可以自己改着玩玩。fov值太高的视野画面空间会被拉长,视野狭窄;fov值太低,空间会被压平,视野比较大。玩过相机的同学应该有体验过广角镜头。 超出摄影机远焦平面和近焦平面的画面内容都会被剪裁掉,也就是不显示。
我们看看在程序中的具体实现(顺便把摄影机的移动和旋转方法更新到较新的版本):
// 窗口属性
int w = 720;
int h = 480;
// 定义顶点
PVector[] ver;
PVector[] face;
PVector[][] uv;
PVector[] cubesPos;
PImage tex;
// 摄影机属性
PVector cameraPos, cameraTarget, up, cameraFront;
float focalLength; // 焦距
float fov, aspect, zNear, zFar;
float yaw, pitch; // 偏航、俯仰
void settings() {
//fullScreen();
size(w, h, P3D);
}
void setup() {
// 设置顶点
ver = new PVector[8];
ver[0] = new PVector(-0.5, -0.5, 0.5); // 顶点1
ver[1] = new PVector(-0.5, -0.5, -0.5); // 顶点2
ver[2] = new PVector( 0.5, -0.5, -0.5); // ...
ver[3] = new PVector( 0.5, -0.5, 0.5);
ver[4] = new PVector(-0.5, 0.5, 0.5);
ver[5] = new PVector(-0.5, 0.5, -0.5);
ver[6] = new PVector( 0.5, 0.5, -0.5);
ver[7] = new PVector( 0.5, 0.5, 0.5);
// 设置顶点索引
face = new PVector[12];
// top
face[0] = new PVector(0, 1, 2);
face[1] = new PVector(0, 2, 3);
// front
face[2] = new PVector(0, 3, 7);
face[3] = new PVector(0, 7, 4);
// back
face[4] = new PVector(1, 2, 6);
face[5] = new PVector(1, 6, 5);
// right
face[6] = new PVector(3, 2, 7);
face[7] = new PVector(2, 6, 7);
// left
face[8] = new PVector(0, 4, 5);
face[9] = new PVector(1, 5, 0);
// bottom
face[10] = new PVector(4, 5, 7);
face[11] = new PVector(5, 6, 7);
// 设置UV
// 目前有一点苦力活需要干,但很快,将会有东西拯救我们。
uv = new PVector[12][3]; // 12个面,每个面3个顶点,为每个顶点描述UV
float max = 2.0;
// top
uv[0][0] = new PVector(0, max); // face0
uv[0][1] = new PVector(0, 0);
uv[0][2] = new PVector(max, 0);
uv[1][0] = new PVector(0, max); // face1
uv[1][1] = new PVector(max, 0);
uv[1][2] = new PVector(max, max);
// front
uv[2][0] = new PVector(0, 0); // face2
uv[2][1] = new PVector(max, 0);
uv[2][2] = new PVector(max, max);
uv[3][0] = new PVector(0, 0); // face3
uv[3][1] = new PVector(max, max);
uv[3][2] = new PVector(0, max);
// back
uv[4][0] = new PVector(0, 0); // face4
uv[4][1] = new PVector(max, 0);
uv[4][2] = new PVector(max, max);
uv[5][0] = new PVector(0, 0); // face5
uv[5][1] = new PVector(max, max);
uv[5][2] = new PVector(0, max);
// right
uv[6][0] = new PVector(0, 0); // face6
uv[6][1] = new PVector(max, 0);
uv[6][2] = new PVector(0, max);
uv[7][0] = new PVector(max, 0); // face7
uv[7][1] = new PVector(max, max);
uv[7][2] = new PVector(0, max);
// left
uv[8][0] = new PVector(0, 0); // face8
uv[8][1] = new PVector(0, max);
uv[8][2] = new PVector(max, max);
uv[9][0] = new PVector(max, 0); // face9
uv[9][1] = new PVector(max, max);
uv[9][2] = new PVector(0, 0);
// bottom
uv[10][0] = new PVector(0, max); // face10
uv[10][1] = new PVector(0, 0);
uv[10][2] = new PVector(max, max);
uv[11][0] = new PVector(0, 0); // face11
uv[11][1] = new PVector(max, 0);
uv[11][2] = new PVector(max, max);
// 设置cubes的位置坐标
cubesPos = new PVector[10];
randomSeed(9999);
for (int i = 0; i < cubesPos.length; i++) {
cubesPos[i] = new PVector(random(-400, 400), random(-100, 100), random(-400, 400));
}
// 设置图形模式
noStroke();
// 载入贴图
tex = loadImage("t3.jpg");
tex.resize(int(256/max), 0);
// 设置纹理属性
textureMode(NORMAL);
textureWrap(REPEAT);
// 设置摄影机
focalLength = (height/2.0) / tan(PI*30.0/180.0);
cameraPos = new PVector(width/2.0, height/2.0, focalLength);
cameraFront = new PVector(0.0, 0.0, -1.0);
up = new PVector(0, 1, 0);
// 投影矩阵参数
fov = radians(60); // 视野(Field of View)
aspect = float(width)/float(height); // 画幅比例
zNear = cameraPos.z/10.0; // 近焦平面
zFar = cameraPos.z*10.0; // 远焦平面
// 摄影机旋转属性
yaw = -90.0;
pitch = 0.0;
//noCursor();
}
void draw() {
// 清楚缓冲区
background(0);
// 事件处理
do_movement();
// 设置摄影机
cameraTarget = PVector.add(cameraPos, cameraFront);
camera(cameraPos.x, cameraPos.y, cameraPos.z,
cameraTarget.x, cameraTarget.y, cameraTarget.z,
up.x, up.y, up.z);
perspective(fov, aspect, zNear, zFar);
// 绘制图形
for (int n = 0; n < cubesPos.length; n++) {
// 分配模型网格位置
translate(cubesPos[n].x, cubesPos[n].y, cubesPos[n].z);
pushMatrix();
// 单个模型矩阵变换
translate(width/2, height/2, -100.0);
scale(200);
// 绘制单个网格物体
//fill(255, 127, 39);
for (int i = 0; i < face.length; i++) {
beginShape();
texture(tex);
// x , y , z , u , v
vertex(ver[int(face[i].x)].x, ver[int(face[i].x)].y, ver[int(face[i].x)].z, uv[i][0].x, uv[i][0].y);
vertex(ver[int(face[i].y)].x, ver[int(face[i].y)].y, ver[int(face[i].y)].z, uv[i][1].x, uv[i][1].y);
vertex(ver[int(face[i].z)].x, ver[int(face[i].z)].y, ver[int(face[i].z)].z, uv[i][2].x, uv[i][2].y);
endShape(TRIANGLES);
}
popMatrix();
}
}
void do_movement() {
float cameraSpeed = 5.0;
// 移动摄影机(同时移动目标)
if (keyPressed) {
// 前后
if (key == 'w') {
cameraPos.z -= cameraSpeed;
cameraTarget.z -= cameraSpeed;
}
if (key == 's') {
cameraPos.z += cameraSpeed;
cameraTarget.z += cameraSpeed;
}
// 左右
if (key == 'a') {
cameraPos.x -= cameraSpeed;
cameraTarget.x -= cameraSpeed;
}
if (key == 'd') {
cameraPos.x += cameraSpeed;
cameraTarget.x += cameraSpeed;
}
}
// 转动摄影机视角
float sensitivity = 0.05; // 灵敏度
if (mousePressed) {
// 偏航
yaw += (mouseX - pmouseX)*sensitivity;
// 俯仰
pitch += (mouseY - pmouseY)*sensitivity;
// 限值俯仰值
if (pitch > 89.0f)
pitch = 89.0f;
if (pitch < -89.0f)
pitch = -89.0f;
// 更新摄影机目标
cameraFront.x = cos(radians(yaw))*cos(radians(pitch));
cameraFront.y = sin(radians(pitch));
cameraFront.z = sin(radians(yaw))*cos(radians(pitch));
cameraFront.normalize();
// 另一种方法,异曲同工,相对理解简单。
// 这种方法不需要cameraFront, 直接修改cameraTarget
// cameraTarget初始化为:(width/2, height/2, 0)
// yaw, pitch 的起始值为:180,180
//cameraTarget.x = sin(radians(yaw))*focalLength + cameraPos.x;
//cameraTarget.y = sin(radians(pitch))*focalLength + cameraPos.y;
//cameraTarget.z = (cos(radians(yaw)) + cos(radians(pitch))) * focalLength + cameraPos.z;
}
// 重置
if (keyPressed) {
if (key == 'r') {
yaw = -90.0;
pitch = 0.0;
cameraFront.x = cos(radians(yaw))*cos(radians(pitch));
cameraFront.y = sin(radians(pitch));
cameraFront.z = sin(radians(yaw))*cos(radians(pitch));
cameraFront.normalize();
}
}
}
关于摄影机的方向控制要花点心思去理解数学。 控制摄影机的方向,如果焦距不变的话,实际上就是在一个球面上运动。 想想这应该也可以用 距离场(distance field)来实现。 这里我有空(恢复点力气)再更新吧,连同位置控制。。。 毕竟我们希望看哪走哪。
好啦,填坑完毕!