Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
forthealllight opened this issue Dec 15, 2019 · 4 comments
Open

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

forthealllight opened this issue Dec 15, 2019 · 4 comments

Comments

@forthealllight
Copy link
Owner

forthealllight commented Dec 15, 2019

优雅的学习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-you
Copy link

webgl + 图形学发展很大啊

@forthealllight
Copy link
Owner Author

webgl + 图形学发展很大啊

hah是的,蛮好玩的

@rick-you
Copy link

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

@forthealllight
Copy link
Owner Author

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

哈哈 一起加油

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants