天天看點

談談Processing 3D世界 四攝影機控制攝影機移動透視

這裡回顧前三節知識。

今天我們來聊一聊攝影機(camera)以及透視矩陣(perspective)。

攝影機

通過第一節的知識,我們已經知道了如何對模型矩陣進行變換,進而可以使物體可以做各種運動。但更多的時候,我們希望的是自己能夠控制視角去觀察物體的變化。引入攝影機的概念,便幫助我們解決了這個需求。

攝影機也叫觀察空間(View Space)。這個概念不用過多解釋,在Processing的3D空間中就相當于我們的眼睛。觀察矩陣把所有的世界坐标轉換到觀察坐标,這些新的坐标是相對攝影機的位置和方向的。

定義一個攝影機,我們需要攝影機在世界空間中的:

  • 位置
  • 觀察方向
  • 一個指向它的右側的向量(右軸)
  • 一個指向它上方的向量(上軸)
談談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);
           
談談Processing 3D世界 四攝影機控制攝影機移動透視

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)來實作。 這裡我有空(恢複點力氣)再更新吧,連同位置控制。。。 畢竟我們希望看哪走哪。

好啦,填坑完畢!