谈谈Processing 3D世界 四
2016-08-10 11:05
495 查看
这里回顾前三节知识。
今天我们来聊一聊摄影机(camera)以及透视矩阵(perspective)。
摄影机也叫观察空间(View Space)。这个概念不用过多解释,在Processing的3D空间中就相当于我们的眼睛。观察矩阵把所有的世界坐标转换到观察坐标,这些新的坐标是相对摄影机的位置和方向的。
定义一个摄影机,我们需要摄影机在世界空间中的:
位置
观察方向
一个指向它的右侧的向量(右轴)
一个指向它上方的向量(上轴)
我们来看看定义:
上向量是用来干吗的呢?有了上向量,我们可以求出摄影机的右轴和上轴,从而创建一个LookAt矩阵帮助我们实现摄影机的功能。
这里说说过程,你只需了解就好,因为他们都由Processing帮助我们计算完成。我们把上向量和第二步得到的摄影机方向向量叉乘。两个向量叉乘的结果就是同时垂直于两向量的向量,因此我们会得到指向x轴正方向的那个向量(如果我们交换两个向量的顺序就会得到相反的指向x轴负方向的向量:
我们只需要向camera()函数中填入摄影机的位置,方向及上向量,就可以一切ok。
这十分简单,我们分别控制摄影机的位置cameraPos, 和摄影机的目标cameraTarget即可。
注意移动摄影机时,要连带移动摄影机目标,否则我们的摄影机只会盯住目标不放= =!。
很多同学不知道如何让物体自旋。有了摄影机这个观察者视角,你想让物体怎么折腾都可以了。
这里有完整代码(这里先简单的改变摄像机):
out = (x/w, y/w, z/w);
在Processing中创建投影矩阵很简单:perspective() 函数。我们来看看具体的方法,投影矩阵要放在摄影机矩阵之后实现:
fov也就是视野(Field of View),相信玩过3D类游戏的朋友应该都清楚。通常设定在45,60,75,90这几个档位。你可以自己改着玩玩。fov值太高的视野画面空间会被拉长,视野狭窄;fov值太低,空间会被压平,视野比较大。玩过相机的同学应该有体验过广角镜头。
超出摄影机远焦平面和近焦平面的画面内容都会被剪裁掉,也就是不显示。
我们看看在程序中的具体实现(顺便把摄影机的移动和旋转方法更新到较新的版本):
关于摄影机的方向控制要花点心思去理解数学。
控制摄影机的方向,如果焦距不变的话,实际上就是在一个球面上运动。
想想这应该也可以用距离场(distance field)来实现。
这里我有空(恢复点力气)再更新吧,连同位置控制。。。
毕竟我们希望看哪走哪。
好啦,填坑完毕!
今天我们来聊一聊摄影机(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 .x, cubesPos .y, cubesPos .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 .x, cubesPos .y, cubesPos .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)来实现。
这里我有空(恢复点力气)再更新吧,连同位置控制。。。
毕竟我们希望看哪走哪。
好啦,填坑完毕!
相关文章推荐
- 谈谈Processing 3D世界 三
- 谈谈Processing 3D世界 五
- 谈谈Processing 3D世界 六 (续二)
- 谈谈Processing 3D世界 一
- 谈谈Processing 3D世界 二
- 谈谈Processing 3D世界 六 (续)
- 谈谈Processing 3D世界 六
- 谈谈Processing 3D世界 四(补充)
- 在3D世界中创建不同的相机模式——创建一个模糊(Blur),(发光)Glow Post-Processing Effect
- 鼠标捡选(窗口坐标系转化为3D世界坐标系)
- 三维世界里的坐标和变换,逆方向旋转移动三维世界的方式来实现3D漫游
- [3D数学]Quake3平面Surface的光照贴图(light map)UV坐标与Surface顶点世界3D坐标之间的转换原理
- 【Stage3D学习笔记续】真正的3D世界(二):显示模型
- 在3D世界中创建不同的相机模式——指定相机的目标
- 在3D世界中创建不同的相机模式——编写自定义内容导入器
- 在3D中, 将世界坐标映射为屏幕上的坐标点
- [文摘20080919]小软件将网页变为3D世界
- 走进VR开发世界(5)—— 使用Cocos开发一款简单的3D VR抓钱游戏
- 通过kinect实现3d扫描建立打印模型(processing、skanect、ReconstructMe)
- 只有4K大小的3D DOS动画 世界编程大赛头名程序