WebGL 正交投影矩阵

1,343 阅读3分钟

源码:github.com/buglas/webg…

WebGL 是一个光栅引擎,其本身并不会实现三维效果,那我们要在其中实现三维效果的关键就在于算法:

顶点在裁剪空间中的位置=投影矩阵*视图矩阵*模型矩阵*顶点的初始点位

正交投影矩阵是投影矩阵的一种,我们先从它说起。

在说正交投影矩阵之前,我们还需要对裁剪空间有一个清晰的认知。

1-裁剪空间

裁剪空间是用于显示webgl图形的空间,此空间是一个宽、高、深皆为2 的盒子。其坐标系的原点在canvas画布的中心,如下图:

image-20211125103531764

裁剪空间中:

  • x轴上-1的位置对应canvas画布的左边界,1的位置对应canvas 画布的右边界
  • y轴上-1的位置对应canvas画布的下边界,1的位置对应canvas 画布的上边界
  • z轴上-1的位置朝向屏幕外部,1的位置朝向屏幕内部,如下图:

image-20211126171042475

2-正交投影矩阵的实现原理

image-20210708084917049

正交投影矩阵 orthographic projection:将世界坐标系中的一块矩形区域(正交相机的可视区域)投射到裁剪空间中,不同深度的物体不具备近大远小的透视规则。

image-20211111223950715

请问:要将一个任意尺寸的长方体塞进裁剪空间里,分几步?

答:先位移,再缩放

image-20211111223844328

设:正交相机可视区域的上、下、左、右、前、后的边界分别是t、b、l、r、n、f

1.位移矩阵

[    1,0,0,-(r+l)/2,    0,1,0,-(t+b)/2,    0,0,1,-(f+n)/2,    0,0,0,1,]

2.缩放矩阵

[    2/(r-l), 0,       0,        0,    0,       2/(t-b), 0,        0,    0,       0,       2/(f-n), 0,    0,       0,       0,        1,]

正交投影矩阵=缩放矩阵*位移矩阵

[    2/(r-l), 0,       0,        -(r+l)/(r-l),    0,       2/(t-b), 0,        -(t+b)/(t-b),    0,       0,       2/(f-n),  -(f+n)/(f-n),    0,       0,       0,        1,]

若n、f是一个距离量,而不是在z轴上的刻度值,正交投影矩阵在z轴上的缩放因子需要取反:

[    2/(r-l), 0,       0,         -(r+l)/(r-l),    0,       2/(t-b), 0,         -(t+b)/(t-b),    0,       0,       -2/(f-n),  -(f+n)/(f-n),    0,       0,       0,         1,]

3-正交投影矩阵的代码实现

正交投影矩阵的代码实现很简单,我们可以直接从three.js 的Matrix4对象的makeOrthographic() 方法中找到:

makeOrthographic( left, right, top, bottom, near, far ) {

    const te = this.elements;
    const w = 1.0 / ( right - left );
    const h = 1.0 / ( top - bottom );
    const p = 1.0 / ( far - near );

    const x = ( right + left ) * w;
    const y = ( top + bottom ) * h;
    const z = ( far + near ) * p;

    te[ 0 ] = 2 * w;    te[ 4 ] = 0;    te[ 8 ] = 0;    te[ 12 ] = - x;
    te[ 1 ] = 0;    te[ 5 ] = 2 * h;    te[ 9 ] = 0;    te[ 13 ] = - y;
    te[ 2 ] = 0;    te[ 6 ] = 0;    te[ 10 ] = - 2 * p;    te[ 14 ] = - z;
    te[ 3 ] = 0;    te[ 7 ] = 0;    te[ 11 ] = 0;    te[ 15 ] = 1;

    return this;
}

以前我们在绘制webgl 图形的时候,它们会随canvas 画布的大小发生拉伸,对于这个问题,我们便可以用投影矩阵来解决。

4-使用正交投影矩阵解决webgl图形拉伸问题

我们先准备一个三角形。

1.顶点着色器

<script id="vertexShader" type="x-shader/x-vertex">
    attribute vec4 a_Position;
    uniform mat4 u_ProjectionMatrix;
    void main(){
      gl_Position = u_ProjectionMatrix*a_Position;
    }
</script>
  • u_ProjectionMatrix 正交投影矩阵

2.片元着色器。

<script id="fragmentShader" type="x-shader/x-fragment">
    precision mediump float;
    uniform vec4 u_Color;
    void main(){
      gl_FragColor=u_Color;
    }
</script>

3.绘制1个三角形

<script type="module">
    import { initShaders } from '../jsm/Utils.js';
    import { Matrix4, Vector3, Quaternion, Object3D, OrthographicCamera } from 'https://unpkg.com/three/build/three.module.js';
    import Poly from './jsm/Poly.js'

    const canvas = document.getElementById('canvas');
    const [viewW, viewH] = [window.innerWidth, window.innerHeight]
    canvas.width = viewW;
    canvas.height = viewH;
    const gl = canvas.getContext('webgl');

    const vsSource = document.getElementById('vertexShader').innerText;
    const fsSource = document.getElementById('fragmentShader').innerText;
    initShaders(gl, vsSource, fsSource);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);

    const projectionMatrix = new Matrix4()

    const triangle = new Poly({
      gl,
      source: [
        0, 0.3, -0.2,
        - 0.3, -0.3, -0.2,
        0.3, -0.3, -0.2
      ],
      type: 'TRIANGLES',
      attributes: {
        a_Position: {
          size: 3,
          index: 0
        },
      },
      uniforms: {
        u_Color: {
          type: 'uniform4fv',
          value: [1, 1, 0, 1]
        },
        u_ProjectionMatrix: {
          type: 'uniformMatrix4fv',
          value: projectionMatrix.elements
        },
      }
    })

    render()

    function render() {
      gl.clear(gl.COLOR_BUFFER_BIT);
      triangle.draw()
    }

</script>

效果如下:

image-20210706213916367

默认情况下,webgl 图形会被canvas 画布所拉伸。

我们可以通过对相机上下左右边界的设置,使其不被canvas 画布所拉伸。

4.定义相机世界高度尺寸的一半

const halfH = 2

5.计算画布的宽高比

const ratio = canvas.width / canvas.height

6.基于halfH和画布宽高比计算相机世界宽度尺寸的一半

const halfW = halfH * ratio

7.定义相机世界的6个边界

const [left, right, top, bottom, near, far] = [    -halfW, halfW, halfH, -halfH, 0, 4]

8.获取正交投影矩阵

projectionMatrix.makeOrthographic(
    left, right, top, bottom, near, far
)

我们利用投影矩阵将现实世界投射到裁剪空间中后,往往还会对裁剪空间中视图进行位移或旋转,这时候就需要视图矩阵了。