WebGL基础教程之 三维透视投影

本教程转载自 https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-3d-perspective.html


此文上接WebGL系列文章,从基础概念开始, 上一篇是三维的基础内容,如果没读过请从那里开始。

上一篇文章讲述了如何实现三维,那个三维用的不是透视投影, 而是的所谓的“正射”投影,但那不是我们日常观看三维的方式。

我们应使用透视投影代替它,但什么是透视投影?它的基础特性就是离得越远显得越小。

在上方的示例中,远处的物体会变小,想要实现例子中近大远小的效果, 简单的做法就是将裁减空间中的 X 和 Y 值除以 Z 值。

你可以这么想:如果一个线段是 (10, 15) 到 (20,15), 它长度为十个单位,在当前的代码中它就是 10 个像素长, 但是如果我们将它除以 Z ,且 Z 值 为 1

10 / 1 = 10
20 / 1 = 20
abs(10-20) = 10

它将是 10 个像素长,如果 Z 值为 2

10 / 2 = 5
20 / 2 = 10
abs(5 - 10) = 5

就是 5 像素了,当 Z 值为 3 时

10 / 3 = 3.333
20 / 3 = 6.666
abs(3.333 - 6.666) = 3.333

你可以看出随着 Z 变大距离就变远了,画的也会小一点。如果我们除以裁剪空间中的 Z ,值可能会变大,因为 Z 是一个较小的值(-1 到 +1)。但是我们可以提供一个 fudgeFactor 因子和 Z 相乘,这样就可以调整缩放的程度。

让我们来试试,首先修改顶点着色器,除以 Z 再乘以我们的 "fudgeFactor" 因子。

<script id="3d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_fudgeFactor;
...
void main() {
  // 将位置和矩阵相乘
  vec4 position = u_matrix * a_position;

  // 调整除数
  float zToDivideBy = 1.0 + position.z * u_fudgeFactor;

  // x 和 y 除以调整后的除数
  gl_Position = vec4(position.xy / zToDivideBy, position.zw);
}
</script>

注意,由于裁减空间中的 Z 值是 -1 到 +1 的,所以 +1 是为了让 zToDivideBy 变成 0 到 +2 * fudgeFactor

还需要更新代码以设置 fudgeFactor。

...
  var fudgeLocation = gl.getUniformLocation(program, "u_fudgeFactor");

  ...
  var fudgeFactor = 1;
  ...
  function drawScene() {
    ...
    // 设置 fudgeFactor
    gl.uniform1f(fudgeLocation, fudgeFactor);

    // 绘制几何体
    var primitiveType = gl.TRIANGLES;
    var offset = 0;
    var count = 16 * 6;
    gl.drawArrays(primitiveType, offset, count);

这是结果(https://webglfundamentals.org/webgl/webgl-3d-perspective.html)。

如果效果不明显,可以将 "fudgeFactor" 滑块从 1.0 拖到 0.0 来对比没添加这些代码之前的样子。

事实上WebGL会将我们提供给 gl_Position 的 x,y,z,w 值自动除以 w 。

我们可以通过修改着色器来证明,用 zToDivideBy 代替 gl_Position.w

...
<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_fudgeFactor;
...
void main() {
  // 将位置和矩阵相乘
  vec4 position = u_matrix * a_position;

  // 调整除数
  float zToDivideBy = 1.0 + position.z * u_fudgeFactor;

  // 将 x y z 除以 zToDivideBy
  gl_Position = vec4(position.xyz, zToDivideBy);

  // 传递颜色到给片断着色器
  v_color = a_color;
}
</script>

看他们多像(https://webglfundamentals.org/webgl/webgl-3d-perspective-w.html)。

为什么WebGL会自动除以 W ?因为使用矩阵的魔力,可以用把值从 z 传值到 w 。

一个这样的矩阵

1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 1,
0, 0, 0, 0,

将会把 z 的值复制给 w , 你可以把每列看作

x_out = x_in * 1 +
        y_in * 0 +
        z_in * 0 +
        w_in * 0 ;

y_out = x_in * 0 +
        y_in * 1 +
        z_in * 0 +
        w_in * 0 ;

z_out = x_in * 0 +
        y_in * 0 +
        z_in * 1 +
        w_in * 0 ;

w_out = x_in * 0 +
        y_in * 0 +
        z_in * 1 +
        w_in * 0 ;

简化后得到

x_out = x_in;
y_out = y_in;
z_out = z_in;
w_out = z_in;

如果 w 原来就是 1.0 就会加 1

1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 1,
0, 0, 0, 1,

他会将 W 的运算变为

w_out = x_in * 0 +
        y_in * 0 +
        z_in * 1 +
        w_in * 1 ;

因为 w_in = 1.0 是已知的

w_out = z_in + 1;

最后可以将 fudgeFactor 像这样放入矩阵中

1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, fudgeFactor,
0, 0, 0, 1,

相当于

w_out = x_in * 0 +
        y_in * 0 +
        z_in * fudgeFactor +
        w_in * 1 ;

简化后为

w_out = z_in * fudgeFactor + 1;

我们来修改代码,使用这个矩阵。

首先将顶点着色器还原,又变成简单的样子

<script id="2d-vertex-shader" type="x-shader/x-vertex">
uniform mat4 u_matrix;

void main() {
  // 位置和矩阵相乘
  gl_Position = u_matrix * a_position;
  ...
}
</script>

接下来定义一个方法实现 Z → W 的矩阵。

function makeZToWMatrix(fudgeFactor) {
  return [
    1, 0, 0, 0,
    0, 1, 0, 0,
    0, 0, 1, fudgeFactor,
    0, 0, 0, 1,
  ];
}

然后使用它:

   ...
    // 计算矩阵
    var matrix = makeZToWMatrix(fudgeFactor);
    matrix = m4.multiply(matrix, m4.projection(gl.canvas.clientWidth, gl.canvas.clientHeight, 400));
    matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
    matrix = m4.xRotate(matrix, rotation[0]);
    matrix = m4.yRotate(matrix, rotation[1]);
    matrix = m4.zRotate(matrix, rotation[2]);
    matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);

    ...

和之前的很像(https://webglfundamentals.org/webgl/webgl-3d-perspective-w-matrix.html):

这只是展示了除以 Z 值获可以实现透视投影,以及在WebGL中简单实现。但还有一些问题需要解决,比如将 Z 值设置为 -100 左右的时候会遇到下面的情形:

为什么会这样?为什么 F 提前消失了?WebGL裁剪空间中的 X 和 Y 会被 +1 和 -1 裁剪, Z也一样。我们看到的是 Z < -1 的情况。

我可以从数学方法深入探讨并寻找解决办法,但是你可以 联想 二维中的的解决方法。我们需要获取 Z 值,然后加上一些量, 缩放一些量,就可以将任意范围映射到 -1 到 +1 的范围内。

最有意思的是这件事可以在一个矩阵中完成,更方便的是, 我们可以定义一个 fieldOfView 代替 fudgeFactor , 计算出更合适的值。

这是创建矩阵的方法。

ar m4 = {
  perspective: function(fieldOfViewInRadians, aspect, near, far) {
    var f = Math.tan(Math.PI * 0.5 - 0.5 * fieldOfViewInRadians);
    var rangeInv = 1.0 / (near - far);

    return [
      f / aspect, 0, 0, 0,
      0, f, 0, 0,
      0, 0, (near + far) * rangeInv, -1,
      0, 0, near * far * rangeInv * 2, 0
    ];
  },

  ...

这个矩阵会为我们完成所有转换。它可以调整单位以适应裁剪空间, 它可以自定义视场角,选择 Z-裁剪面。假设有一个眼睛或者摄像机 在原点(0, 0, 0),根据 zNear和 fieldOfView 可以将 zNear 对应到 Z = -1 ,在 zNear 平面上一半的 fieldOfView 长度 对应画布中心到 Y = -1 或 Y = 1 的距离,X 的值通过乘以aspect 获取,最后通过设置 zFar 对应 Z = 1 ,控制缩放的程度。

这是矩阵的图解(https://webglfundamentals.org/webgl/frustum-diagram.html):

正方体所在的有四个侧面的椎体叫做“视锥”,矩阵将视锥中的空间转换到裁剪空间中, zNear 决定了被正面切割的位置,zFar 决定被背面切割的位置。 将 zNear设置为 23 就会看到正方体正面被切割, 将 zFar 设置为 24 就会看到正方体背面被切割。

还有一个问题,矩阵假定观察位置为 0,0,0 并且看向 Z 轴负方向, Y 轴为上方向。这和我们目前为止做法不同, 为了解决这个问题我们需要将物体放到视图范围内。

我们在 (45, 150, 0) 绘制的 F,可以将它移动到 (-150, 0, -360)

使用 m4.projection 方法代替之前的投影方法,可以调用 m4.perspective

var aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
var zNear = 1;
var zFar = 2000;
var matrix = m4.perspective(fieldOfViewRadians, aspect, zNear, zFar);
matrix = m4.translate(matrix, translation[0], translation[1], translation[2]);
matrix = m4.xRotate(matrix, rotation[0]);
matrix = m4.yRotate(matrix, rotation[1]);
matrix = m4.zRotate(matrix, rotation[2]);
matrix = m4.scale(matrix, scale[0], scale[1], scale[2]);

结果在这里(https://webglfundamentals.org/webgl/webgl-3d-perspective-matrix.html):

我们讲了矩阵乘法,视角和自定义 Z 范围。还有很多没讲完, 但这篇文章已经很长了,所以接下来继续讲相机。

前端qq群:850038125

前端微信群:

10.WebGL 三维正射投影

9.WebGL 理论基础 - 二维矩阵

8.WebGL 理论基础 - 二维缩放

7.WebGL 理论基础 - 二维旋转

6.WebGL 理论基础 - 二维平移

5.WebGL 理论基础 - 图像处理 下

4.WebGL 理论基础 - 图像处理 上

3.WebGL 理论基础 - 着色器和 GLSL

2.WebGL 理论基础 - 工作原理

1.WebGL 理论基础 - 基础概念

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值