谈谈Processing 3D世界 四

这里回顾前三节知识


今天我们来聊一聊摄影机(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)来实现。
这里我有空(恢复点力气)再更新吧,连同位置控制。。。
毕竟我们希望看哪走哪。

好啦,填坑完毕!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值