我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
本文标题:WebGL第四十课:3D前置知识点之 View 矩阵
友情提示
这篇文章是WebGL课程专栏的第40篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。
引子
如果我们想要绘制什么东西,那么这个东西本身的顶点数据必须事先准备好。
这里有两个路子:
-
- 建模师负责创作模型,然后以某种格式交到你手里,你的程序进行读取
-
- 你的程序使用某种算法自动生成一些模型
不管是哪一种,模型生成之后,传递到WebGL里之后,这一部分数据就是不会再动了(一般而言不会)。
模型坐标的不动会带来两个问题:
-
- 这部分数据的
xyz不一定是, 而且大概率不会是。建模师可不管什么WebGL需要的数据范围。
- 这部分数据的
-
- 如果我们要实现在不同视角观察同一个物体的话,比如说,物体本身不动,而观察的视角从左向右慢慢平移(类似走马灯),我们看见的物体是从右向左慢慢变化的。
以上两个问题,都使得我们不得不去做一件事情:
- 让模型坐标
"变化",以完成我们需求的效果
走马灯效果讨论
走马灯效果可以有两种实现办法:
-
- 让物体本身
从右向左动。
- 让物体本身
-
- 让观察者
从左向右动。
- 让观察者
不管是哪一种实现方式,从WebGL的vertex_shader这一层来看,无非就是要影响一个关键的变量:
gl_Position
我们能改变的也只能是这个变量。再重申一句:模型本身是不可能变的(除非你重新上传模型到WebGL)。
而所谓的3D效果就是,根据一大堆参数,将不动的模型坐标翻过来覆过去的计算,算出一个符合需求的值,输出到gl_Position中。最终如果,算出的坐标点的xyz如果落在区间内,我们就可以在屏幕上看见了。
采用观察者角度实现走马灯(左右上下)
我们现在使用代码生成一个矩形的模型,矩形的四个点的xy坐标如下:
-1, -1,
1, -1,
1, 1,
-1, 1
如果我们直接输出到gl_Position的话,很明显,就是填满整个画布。
此时此刻,我们给我们的场景增加一个关键的参数:
- 相机 Camera
我们用这个参数来代表观察视角和位置。
一开始为简单起见,Camera 只有位置属性,先不讨论它的朝向问题。
我们来具体化需求:
- Camera.xy = (0, 0) 的时候,我们就看见一个铺满画布的矩形(上述模型已给出)
- Camera.x 增加,相当于观察者在向
右平移,那么我们应该看见画布内的矩形往左移动 - Camera.x 减小,相当于观察者在向
左平移,那么我们应该看见画布内的矩形往右移动
不管如何,先来创建一个 class Camera吧。
class Camera {
constructor(x,y) {
this.x = x;
this.y = y;
}
}
根据前面所述:最终要影响的东西是vertex_shader中的gl_Position变量。
很明显,我们需要将 Camera.xy 传给 vertex_shader。
又很明显,我们可以使用 uniform 变量,来传递。
如下:
precision mediump int;
precision mediump float;
attribute vec2 a_PointVertex; // 顶点坐标
uniform vec2 uni_Camera; // 相机的 xy 坐标
void main() {
gl_Position = vec4(?, ?, 0.0, 1.0);
}
观察上面代码,我们这里还是先忽略 gl_Position.z。因为本需求中,走马灯只要考虑左右上下动就行。
问题就来了,如何使用uni_Camera 和 a_PointVertex来进行正确的计算呢?
只需要一点浅显的数学知识:
gl_Position = vec4(a_PointVertex.x - uni_Camera.x,
a_PointVertex.y - uni_Camera.y,
0.0, 1.0);
那么此时此刻,只需要在javascript代码里,正确的将uni_Camera变量传递进来就行了。
关键代码:
// 获取vertex中,uni_Camera 变量的位置
let camera_location = gl.getUniformLocation(program, "uni_Camera");
// 传递数据给这个变量
gl.uniform2f(camera_location, camera.x, camera.y)
整体示例代码,码上掘金: WebGL课程40-1 - 码上掘金 (juejin.cn)
Camera的作用
上述例子,Camera 的所有作用好像只是,用来对顶点坐标进行了一个偏移操作。
然后模拟出,眼睛平移的时候,视觉里的东西移动的效果。
这个行为到底是在做什么?
一句话说明这个东西:
在 Camera 视角下,物体的坐标是啥?
就这一句话,道明了Camera的作用。
我们其实在vertex_shader里做了这样的事情:
- 计算出
Camera视角下,顶点的最终坐标是啥。 - 将这个
最终坐标传递给gl_Position。
上面的做法就有这种好处:
Camera一旦动了,那么我们看见的所有东西也就动了
从而模拟了,眼睛动,画面动的物理效果。
终极Camera的样子
我们上面只是讲述了Camera平移,顶点坐标会如何变化。
真正Camera还包含一个重要的属性,就是朝向。
- 位置
- 朝向
上面两个属性,就是Camera最终的样子。
Camera 的朝向问题
想象一个场景:
把相机的中心钉在一个钉子上,然后转动这个相机。
明显,相机最终的成像是在变化的。
虽然相机指向的位置没变。
所以朝向分成两个属性:
- 相机的镜头指向
- 相机正上方指向
我们知道,如果知道相机的位置,那么只需要指定一个目标位置,就可以算出镜头指向
camera_forward = camera_target - camera_pos;
以上是一个简单的向量运算。
好了,我们只需要指定一下相机的正上方指向,就可以得出相机的朝向了。
我们来综合一下信息:
- 相机位置
camera_pos - 相机正在看什么位置
camera_target - 相机的正上方指向
camera_up
这三个信息就包含了相机的一切信息。
由这三个信息,可以得出一个大名鼎鼎的 View 矩阵, 任何顶点坐标在经过这个矩阵运算之后,都可以得出用Camera视角来看这个顶点坐标的相应坐标。
亦即:
相机视角下某顶点的坐标 = View_矩阵 * 模型某顶点坐标
将这个矩阵传递给vertex_shader,就包含了相机的移动,旋转,等各个操作。我们就不用傻乎乎的写什么偏移啦等操作了。
那么这么有用的矩阵,到底是啥:
function lookAt(camera_pos, camera_target, camera_up) {
var zAxis = normalize(
subtractVectors(camera_pos, camera_target));
var xAxis = normalize(cross(camera_up, zAxis));
var yAxis = normalize(cross(zAxis, xAxis));
return [
xAxis[0], xAxis[1], xAxis[2], 0,
yAxis[0], yAxis[1], yAxis[2], 0,
zAxis[0], zAxis[1], zAxis[2], 0,
camera_pos[0],
camera_pos[1],
camera_pos[2],
1,
];
}
对,一般情况下,获取这个矩阵用的函数名叫lookAt。
矩阵里面具体是怎么得来的,这是数学问题,这里忽略讨论,网上可以找到。
总结
一张图:
正文结束,下面是答疑
小能能说:View 矩阵就是将 模型的坐标变成 视角中的坐标
- 对。
小能能说:依旧无法保证 ?
- 对。你咋这么能呢。
小能能说:咋弄?
- Projection 矩阵
小能能说: Projection 矩阵用来保证?
- 然也,非也。