forthealllight / blog

📖我的博客,记录学习的一些笔记,如有喜欢,欢迎star
2.3k stars 225 forks source link

优雅的学习webgl(4)—webgl中的投影模型 #53

Open forthealllight opened 4 years ago

forthealllight commented 4 years ago

优雅的学习webgl(4)—webgl中的投影模型


    在前一章中我们介绍了三维图形基础,提到了要想在画布中渲染出三维图形,必须同时确定视图矩阵和投影矩阵,这章我们来详细的介绍一下什么是投影模型。

  • webgl中的坐标系统
  • 什么是投影
  • 正射投影
  • 透视投影

这个系列的源码地址为:源码的地址为: https://github.com/forthealllight/webgl-demo

一、webgl中的坐标系统

    在了解投影之前,我们现来看一下webgl中的坐标系统,在webgl中有两个坐标系统,一个是裁剪面坐标,另一个是纹理坐标。其中跟旋转变换以及图形渲染可视有关的是裁剪面坐标,这里我们先介绍webgl中的裁剪面坐标,而纹理坐标会在纹理贴图章节介绍。

webgl中的裁剪面坐标如下所示:

Lark20191216-181530

    从上述裁剪坐标系统的示意图中我们可以看出,webgl中整个裁剪平面的中心坐标是(0,0),对于二维的裁剪平面而言其水平方向从(-1,0)到(1,0),其竖直方向从(0,-1)到(0,1).

也就是说其裁剪坐标任何方向的值在区间[-1,1]内。

注意一点:裁剪平面是决定如何映射到画布上,因为画布是二维的,因此裁剪平面也是二维的。webgl中z轴的坐标并不限制在(-1,1),可以为任何值

那么画布坐标呢,比如我们以canvans640 X 480大小的画布为例:

Lark20191216-205917

画布坐标的原点位于左下角。在webgl中我们推荐对于所有的点使用裁剪面坐标来表示,这样比较方便绘制,如何将裁剪面坐标表示的点展示到画布上呢,可以通过webgl的函数来实现:

gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);

有些场景比较特殊,我们将在webgl中使用像素坐标(画布坐标系下的坐标),那么需要额外的操作,在顶点着色器中,需要将画布坐标转化为裁剪平面坐标,转化的例子如下,在顶点着色器中我们使用:

uniform vec2 u_resolution; 用于读取画布的坐标,比如(640,480)向量

  void main() {
    // 从像素坐标转换到 0.0 到 1.0
    vec2 zeroToOne = a_position / u_resolution;

    // 再把 0->1 转换 0->2
    vec2 zeroToTwo = zeroToOne * 2.0;

    // 把 0->2 转换到 -1->+1 (裁剪空间)
    vec2 clipSpace = zeroToTwo - 1.0;

    gl_Position = vec4(clipSpace, 0, 1);
  }

这样,我们在其他webgl的代码中就可以使用像素坐标,下面是一个使用像素坐标来绘制一个三角形的例子:

以640 * 480 的canvas画布为例,像素坐标系下的点:

positions = [
    160, 120,
    320, 360,
    480, 120,
]

和裁剪平面下的坐标点:

positions = [
    0.0,0.5,
    -0.5,-0.5,
    0.5,-0.5
]

这两组坐标是等价的。具体例子可见:

https://github.com/forthealllight/webgl-demo/tree/master/demo7

二、什么是投影

    在第三章我们将到了视图矩阵,得出一个公式就是:

观测到的经过矩阵变换三维图形的坐标 = <视图矩阵> x <模型矩阵> 图形的原始坐标

    这个模型下,我们认为无论观测者位于哪里,都可以看到被观测的物体,我们简单的认为可视空间是无限大的。显然这样是不对的,因为在现实生活中,在北京的人不可能可以观测到上海的物体。限定可视区的模型就是投影。

    投影有两种,一种是正射投影,也就是说在在投影范围内,看到物体是等大小的。另一种是透视投影,遵循近大远小的规则。现实生活中,透视投影比较贴近。

有些时候我们可能搞混淆了视图矩阵和投影模型矩阵,其实有一句诗可以帮助我们理解:

横看成岭侧成峰,远近高低各不同

    前半句就是视点不同,从而导致视图矩阵不同得到不同的观测结果,后半句就是更多侧重于投影规则引起的近大远小。

引入投影模型后,完整的图形渲染公式应该是:

观测到的经过矩阵变换三维图形的坐标 = <投影矩阵> x <视图矩阵> x <模型矩阵> 图形的原始坐标

下节我们会详细的介绍正射投影和透视投影。

三、正射投影

    正射投影就是可视区近处和远处看到的物体是等大小的。由正射投影模型产生的可视区称为盒状可视区,盒状可视区模型如下:

Lark20191217-200354

    盒装可视区就是一个三维长方体,近裁剪面就是能看到的最近的z轴距离,远裁剪面就是能看到的最远的z轴的距离,在远近裁剪面之间的三维长方体就是可视区。

    我们从模型中可以看出,这个正射投影矩阵或者说可视矩阵由6个坐标来决定,分别是near,top,left,right,bottom,far。

完整的三维图形观测渲染模型为:

观测到的经过矩阵变换三维图形的坐标 = <正射投影矩阵> x <视图矩阵> x <模型矩阵> 图形的原始坐标

下面我们来绘制一个在正射投影模型下的三维立方体:


  const zNear = 3;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();

   mat4.ortho(projectionMatrix,
    -1,
    1,
    -1,
    1,
    zNear,
    zFar);

上述的代码就是构建了<正射投影矩阵> x <视图矩阵>的过程。具体的例子可以看:

https://github.com/forthealllight/webgl-demo/tree/master/demo8

绘制的图案如图所示: Lark20191218-191757

这个正方体太大了,并且因为是正射投影的原因,因此只能在画布中看到正方体的一部分。

四、透视投影

    透视投影模型就是复合我们常识的近大远小的观测模型,我们来看透视模型的示意图:

Lark20191218-192710

    从上述的透视投影模型来看,它的可视区不是一个长方体,而是一个棱柱,是底面是大小不想等的长方形,在透视投影模型下的可视区又称为金字塔可视空间。

我们同样的在金字塔可视空间内绘制同样的一个正方体,修改代码使其变成金字塔模型

 const fieldOfView = 45 * Math.PI / 180;   // in radians
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 3;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();
  mat4.perspective(projectionMatrix,
       fieldOfView,
       aspect,
       zNear,
       zFar);

最后的绘制结果为:

Lark20191217-205150

    显然这是不符合我们预期的,我们立方体的8个面分别有8个颜色,从上述的例子中我们不仅可以看到正面,还能看到背面的正方形,这显然是不符合人眼观测的,为了消除这种异常,我们要了解一下深度缓存。

    我们前面降到在裁剪平面中的xy值的范围是-1到1,而对于z轴不做处理,或者没有值的范围。这句话说对也对,说不对也不对。 诚然裁剪面内不会对z轴做任何处理,但是在webgl的深度缓存中会自动对z的深度做处理,将z值转化到0->1。那么这种自动对于深度的处理如何转化到裁剪平面,从而影响绘制结果呢,让我们只能看到正面而看不到背面。

    答案就是,在深度缓存中支持深度检测,当检测到在同样位置第二次绘制的图形的深度小于第一次,那么就不会绘制,如何第二次绘制图形的深度大于第一次,那么会重新绘制并覆盖第一次。

    这里的深度其实要加引号,深度越大,其实是离观察者越近,因此深度大的优先显示。

开启深度检测也很简单,只需要:

//开启深度检测
gl.enable(gl.DEPTH_TEST);  
//在绘制前清空深度缓存
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

进行深度检测后绘制的立方体的图形如下所示:

Lark20191217-211547

绘制出上述的正方体后,我们还是没能体现出透视投影近大远小的成像原理,我们可以尝试平移我们的视点位置,在z轴方向平移,

平移前:

  mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -6.0]); 

平移后:

 mat4.translate(modelViewMatrix, modelViewMatrix, [-0.0, 0.0, -16.0]); 

视点的位置从(0,0,-6)平移到了(0,0,-16),平移后我们来看渲染结果,负像的位移是使得视点远离物体的,最后的渲染结果为:

Lark20191218-193854

从直观上来看,正方体变小了,这就是我们所说的在透视投影模型中的近大远小。

本例的地址为:https://github.com/forthealllight/webgl-demo/tree/master/demo9

rick-yo commented 4 years ago

webgl + 图形学发展很大啊

forthealllight commented 4 years ago

webgl + 图形学发展很大啊

hah是的,蛮好玩的

rick-yo commented 4 years ago

我也想往这方面靠靠,做做游戏 flutter 引擎啥的,加油

forthealllight commented 4 years ago

我也想往这方面靠靠,做做游戏 flutter 引擎啥的,加油

哈哈 一起加油