這裡回顧前三節知識。
今天我們來聊一聊攝影機(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)來實作。 這裡我有空(恢複點力氣)再更新吧,連同位置控制。。。 畢竟我們希望看哪走哪。
好啦,填坑完畢!