codecamp

WebGL 3D 透视

WebGL 3D 透视

在上一篇文章中,我们就学习过如何做三维,但三维没有任何透视。它是利用一个所谓的“正交”的观点,它固然有其用途,但这通常不是人们说 “3D” 时他们想要的。

相反,我们需要补充透视。只不过什么是透视?它基本上是一种事物越远显得更小的特征。

看上面的例子,我们看到越远的东西被画得越小。鉴于我们目前样品的一个让较远的物体显得更小简单的方法就是将 clipspace X 和 Y 除以 Z。

可以这样想:如果你有一条从(10,15)到 (20,15) 的线段,10个单位长。在我们目前的样本中,它将绘制 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 的增加,随着它变得越来越远,我们最终会把它画得更小。如果我们在 clipspace 中除,我们可能得到更好的结果,因为 Z 将是一个较小的数字(-1 到 +1)。如果在除之前我们加一个 fudgeFactor 乘以 Z,对于一个给定的距离我们可以调整事物多小。

让我们尝试一下。首先让我们在乘以我们的 “fudgefactor” 后改变顶点着色器除以 Z 。

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_fudgeFactor;
...
void main() {
  // Multiply the position by the matrix.
  vec4 position = u_matrix * a_position;

  // Adjust the z to divide by
  float zToDivideBy = 1.0 + position.z * u_fudgeFactor;

  // Divide x and y by z.
  gl_Position = vec4(position.xy / zToDivideBy, position.zw);
}
</script>

注意,因为在 clipspace 中 Z 从 -1 到 +1,我加 1 得到 zToDivideBy 从 0 到 +2 * fudgeFactor

我们也需要更新代码,让我们设置 fudgeFactor。

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

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

    // Draw the geometry.
    gl.drawArrays(gl.TRIANGLES, 0, 16 * 6);

下面是结果。

如果没有明确的把 “fudgefactor” 从 1 变化到 0 来看事物看起来像什么样子在我们除以 Z 之前。

WebGL 在我们的顶点着色器中把 X,Y,Z,W 值分配给 gl_Position 并且自动除以 W。

我们可以证明通过改变着色这很容易实现,而不是自己做除法,在 gl_Position.w 中加 zToDivideBy

<script id="3d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_fudgeFactor;
...
void main() {
  // Multiply the position by the matrix.
  vec4 position = u_matrix * a_position;

  // Adjust the z to divide by
  float zToDivideBy = 1.0 + position.z * u_fudgeFactor;

  // Divide x, y and z by zToDivideBy
  gl_Position = vec4(position.xyz,  zToDivideBy);
}
</script>

看看这是完全相同的。

为什么有这样一个事实: 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;

我们可以加 1 我们之前用的这个矩阵,因为我们知道 w_in 总是 1.0。

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() {
  // Multiply the position by the matrix.
  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,
  ];
}

我们将更改代码,以使用它。

    ...
    // Compute the matrices
    var zToWMatrix =
        makeZToWMatrix(fudgeFactor);

    ...

    // Multiply the matrices.
    var matrix = matrixMultiply(scaleMatrix, rotationZMatrix);
    matrix = matrixMultiply(matrix, rotationYMatrix);
    matrix = matrixMultiply(matrix, rotationXMatrix);
    matrix = matrixMultiply(matrix, translationMatrix);
    matrix = matrixMultiply(matrix, projectionMatrix);
    matrix = matrixMultiply(matrix, zToWMatrix);

    ...

注意,这一次也是完全相同的。

以上基本上是向你们展示,除以 Z 给了我们透视图,WebGL 方便地为我们除以 Z。    

但是仍然有一些问题。例如如果你设置 Z 到 -100 左右,你会看到类似下面的动画。

发生了什么事?为什么 F 消失得很早?就像 WebGL 剪辑 X 和 Y 或+ 1 到 - 1 它也剪辑 Z。这里看到的就是 Z<-1 的地方。

我可以详细了解如何解决它,但你可以以我们做二维投影相同的方式来得到它。我们需要利用 Z,添加一些数量和测量一定量,我们可以做任何我们想要得到的 -1 到 1 的映射范围。

真正酷的事情是所有这些步骤可以在 1 个矩阵内完成。甚至更好的,我们来决定一个 fieldOfView 而不是一个 fudgeFactor,并且计算正确的值来做这件事。

这里有一个函数来生成矩阵。

function makePerspective(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
  ];
};

这个矩阵将为我们做所有的转换。它将调整单位,所以他们在clipspace 中它会做数学运算,因此我们可以通过角度选择视野,它会让我们选择我们的 z-clipping 空间。它假定有一个 eye 或 camera 在原点(0,0,0)并且给定一个 zNear 和 fieldOfView 计算它需要什么,因此在 zNear 的物体在 z = - 1 结束以及在 zNear 的物体它们在中心以上或以下半个 fieldOfView,分别在 y = - 1 和 y = 1 结束。计算 X 所使用的只是乘以传入的 aspect。我们通常将此设置为显示区域的 width / height。最后,它计算出在 Z 区域物体的规模,因此在 zFar 的物体在 z = 1 处结束。

下面是动作矩阵的图。

形状像四面锥的立方体旋转称为“截锥”。矩阵在截锥内占空间并且转换到 clipspace。zNear 定义夹在前面的物体,zfar定义夹在后面的物体。设置 zNear 为23你会看到旋转的立方体的前面得到裁剪。设置 zFar 为24你会看到立方体的后面得到剪辑。

只剩下一个问题。这个矩阵假定有一个视角在 0,0,0 并假定它在Z轴负方向,Y的正方向。我们的矩阵到目前为止已经以不同的方式解决问题。为了使它工作,我们需要我们的对象在前面的视图。

我们可以通过移动我们的 F 做到。 我们在(45,150,0)绘图。让我们将它移到(0,150,- 360)

现在,要想使用它,我们只需要用对 makePerspective 的调用取代对make2DProjection 旧的调用

    var aspect = canvas.clientWidth / canvas.clientHeight;
    var projectionMatrix =
        makePerspective(fieldOfViewRadians, aspect, 1, 2000);
    var translationMatrix =
        makeTranslation(translation[0], translation[1], translation[2]);
    var rotationXMatrix = makeXRotation(rotation[0]);
    var rotationYMatrix = makeYRotation(rotation[1]);
    var rotationZMatrix = makeZRotation(rotation[2]);
    var scaleMatrix = makeScale(scale[0], scale[1], scale[2]);

结果如下

我们回到了一个矩阵乘法,我们得到两个领域的视图,我们可以选择我们的 z 空间。受篇幅限制我们没有做。下一节,摄像机。

WebGL 3D 正交
WebGL 3D 摄像机
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }