WebGL第四十课:3D前置知识点之 View 矩阵

1,500 阅读6分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

本文标题:WebGL第四十课:3D前置知识点之 View 矩阵

友情提示

这篇文章是WebGL课程专栏的第40篇,强烈建议从前面开始看起。因为花了大量的工夫来讲解向量的概念和矩阵运算。这些基础知识会影响你的思维。

引子

如果我们想要绘制什么东西,那么这个东西本身的顶点数据必须事先准备好。

这里有两个路子:

    1. 建模师负责创作模型,然后以某种格式交到你手里,你的程序进行读取
    1. 你的程序使用某种算法自动生成一些模型

不管是哪一种,模型生成之后,传递到WebGL里之后,这一部分数据就是不会再动了(一般而言不会)。

模型坐标的不动会带来两个问题:

    1. 这部分数据的xyz不一定是[1,1][-1, 1], 而且大概率不会是[1,1][-1, 1]。建模师可不管什么WebGL需要的数据范围。
    1. 如果我们要实现在不同视角观察同一个物体的话,比如说,物体本身不动,而观察的视角从左向右慢慢平移(类似走马灯),我们看见的物体是从右向左慢慢变化的。

以上两个问题,都使得我们不得不去做一件事情:

  • 让模型坐标"变化",以完成我们需求的效果

走马灯效果讨论

走马灯效果可以有两种实现办法:

    1. 让物体本身从右向左动。
    1. 让观察者从左向右动。

不管是哪一种实现方式,从WebGLvertex_shader这一层来看,无非就是要影响一个关键的变量:

  • gl_Position

我们能改变的也只能是这个变量。再重申一句:模型本身是不可能变的(除非你重新上传模型到WebGL)。

而所谓的3D效果就是,根据一大堆参数,将不动的模型坐标翻过来覆过去的计算,算出一个符合需求的值,输出到gl_Position中。最终如果,算出的坐标点的xyz如果落在[1,1][-1,1]区间内,我们就可以在屏幕上看见了。

采用观察者角度实现走马灯(左右上下)

我们现在使用代码生成一个矩形的模型,矩形的四个点的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_Cameraa_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。 矩阵里面具体是怎么得来的,这是数学问题,这里忽略讨论,网上可以找到。

总结

一张图:

image.png




  正文结束,下面是答疑

小能能说:View 矩阵就是将 模型的坐标变成 视角中的坐标

  • 对。

小能能说:依旧无法保证 [1,1][-1 ,1]?

  • 对。你咋这么能呢。

小能能说:咋弄?

  • Projection 矩阵

小能能说: Projection 矩阵用来保证[1,1][-1, 1]?

  • 然也,非也。