WebGL-学习笔记(四)

WebGL学习笔记(四).png

最近学习构建三维图形的时候,深感几何功底不够,一个视图变化矩阵看了几天也没想过来,只勉强理解原理,细节部分自己还需要加强学习

1. 视图变换

在二维图形绘制的时候,不用考虑z轴,但是绘制的三维图形处于一个立体空间,就要使用z轴了。由于存在z轴,那么在物体观察的时候首先就是要确定观察坐标系

1.1 视点,观察点,正方向

观察坐标系可以通过定义:视点,观察点,正方向确定

视点:是眼睛所在的位置,可以认为是观察坐标系的原点
观察点:是观察对象所在位置,虽然观察对象是一个三维物体,但是物体都是有点组成,那么可以确认任何一个点作为观察点
视线:视点和观察点之间的连线
正方向:我的理解是观察坐标系下y轴的正方向,也就是当人观察事物的时候,头顶的朝向

当确定好以上三个要素以后,就可以确定一个观察的坐标系了,为什么?
目前使用坐标系都是直角坐标系,也就是在y轴和视线方向确定好以后,y轴和视线组成了一个平面,那么x轴需要和这个平面垂直,并且经过视点(因为默认视点为原点),之后z轴需要和y轴还有x轴垂直,所以也就确定下来了。
在使用WebGL绘制点的时候都是使用的WebGL的坐标系,但是现在的视点并不一定是WebGL坐标系中的原点,所以这个时候,需要把视点转换为WebGL坐标系中的原点,才能正确的利用WebGL绘制图形,将视点转换为WebGL原点的且y轴正方向和WebGL的y轴重合的变换矩阵就是视图矩阵。
连续看了几天也没有特别明白这个转换矩阵是如何运作的,所以这里先暂时贴出公式(代码摘抄自WebGL编程指南)

Matrix4.prototype.setLookAt = function(eyeX, eyeY, eyeZ, centerX,     centerY, centerZ, upX, upY, upZ) {
  var e, fx, fy, fz, rlf, sx, sy, sz, rls, ux, uy, uz;
  fx = centerX - eyeX;
  fy = centerY - eyeY;
  fz = centerZ - eyeZ;
  // Normalize f.
  rlf = 1 / Math.sqrt(fx*fx + fy*fy + fz*fz);
  fx *= rlf;
  fy *= rlf;
  fz *= rlf;
  // Calculate cross product of f and up.
  sx = fy * upZ - fz * upY;
  sy = fz * upX - fx * upZ;
  sz = fx * upY - fy * upX;
  // Normalize s.
  rls = 1 / Math.sqrt(sx*sx + sy*sy + sz*sz);
  sx *= rls;
  sy *= rls;
  sz *= rls;
  // Calculate cross product of s and f.
  ux = sy * fz - sz * fy;
  uy = sz * fx - sx * fz;
  uz = sx * fy - sy * fx;
  // Set to this.
  e = this.elements;
  e[0] = sx;
  e[1] = ux;
  e[2] = -fx;
  e[3] = 0;
  e[4] = sy;
  e[5] = uy;
  e[6] = -fy;
  e[7] = 0;
  e[8] = sz;
  e[9] = uz;
  e[10] = -fz;
  e[11] = 0;
  e[12] = 0;
  e[13] = 0;
  e[14] = 0;
  e[15] = 1;
  // Translate.
  return this.translate(-eyeX, -eyeY, -eyeZ);
};
Matrix4.prototype.translate = function(x, y, z) {
  var e = this.elements;
  e[12] += e[0] * x + e[4] * y + e[8]  * z;
  e[13] += e[1] * x + e[5] * y + e[9]  * z;
  e[14] += e[2] * x + e[6] * y + e[10] * z;
  e[15] += e[3] * x + e[7] * y + e[11] * z;
  return this;
};

PS:公式主要是将点进行了反向的旋转和平移变换

2. 可视范围

我们观察时候的坐标系是我们自定定义的尺度,而WebGL坐标系中坐标范围是(-1.0,1.0),一旦超出的坐标就不会绘制了,从而导致图形缺失。如果要让视野所见的所有内容都包含在坐标系中,就可能需要对坐标进行缩放和平移,来改变可视范围,常用的方式有两种

2.1 正射投影

长方体的可视范围,是一种盒状空间,用于建筑平面设计。利用近裁剪面和远裁剪面来确定可视区域,因此使用六个参数可以确定正射投影:left,right,top,bottom,near,far,具体正射投影矩阵构建公式如下:

Matrix4.prototype.setOrtho = function(left, right, bottom, top, near, far) {
  var e, rw, rh, rd;
  if (left === right || bottom === top || near === far) {
    throw 'null frustum';
  }
  rw = 1 / (right - left);
  rh = 1 / (top - bottom);
  rd = 1 / (far - near);
  e = this.elements;
  e[0]  = 2 * rw;
  e[1]  = 0;
  e[2]  = 0;
  e[3]  = 0;
  e[4]  = 0;
  e[5]  = 2 * rh;
  e[6]  = 0;
  e[7]  = 0;
  e[8]  = 0;
  e[9]  = 0;
  e[10] = -2 * rd;
  e[11] = 0;
  e[12] = -(right + left) * rw;
  e[13] = -(top + bottom) * rh;
  e[14] = -(far + near) * rd;
  e[15] = 1;
  return this;
};

2.2 透视矩阵

透视矩阵是四棱锥的可视空间,一般用于游戏设计,符合现实场景(近大远小的效果)。也存在近裁剪面和远裁剪面,需要4个参数来确定相关变换矩阵:fovy(可视空间顶面和底面的夹角),aspect(裁剪面高宽比),near,far

Matrix4.prototype.setPerspective = function(fovy, aspect, near, far) {
  var e, rd, s, ct;
  if (near === far || aspect === 0) {
    throw 'null frustum';
  }
  if (near <= 0) {
    throw 'near <= 0';
  }
  if (far <= 0) {
    throw 'far <= 0';
  }
  fovy = Math.PI * fovy / 180 / 2;
  s = Math.sin(fovy);
  if (s === 0) {
    throw 'null frustum';
  }
  rd = 1 / (far - near);
  ct = Math.cos(fovy) / s;
  e = this.elements;
  e[0]  = ct / aspect;
  e[1]  = 0;
  e[2]  = 0;
  e[3]  = 0;
  e[4]  = 0;
  e[5]  = ct;
  e[6]  = 0;
  e[7]  = 0;
  e[8]  = 0;
  e[9]  = 0;
  e[10] = -(far + near) * rd;
  e[11] = -1;
  e[12] = 0;
  e[13] = 0;
  e[14] = -2 * near * far * rd;
  e[15] = 0;
  return this;
};

3. 深度处理

3.1 隐藏面消除

WebGL默认深度的并不会对深度进行处理,会按照我们对点/面的绘制顺序进行绘制,也就最后绘制的内容会在最前面,违背了本来的意图,不过WebGL提供了对应的方法来处理深度关系:

首先,利用gl.enable(gl.DEPTH_TEST)开启隐藏面消除功能
然后,利用深度清理gl.clear(gl.DEPTH_BUFFER_BIT),可以让WebGL自己处理好深度关系

3.2 深度冲突

由于存在两个平面处在同一个深度的情况,这个时候WebGL绘制会出现深度冲突,表现为图形绘制结果看上去表面斑驳,于是WebGL提供了多边形偏移的功能,让即使深度一致的两个表面也会发生一定深度的偏移

首先,利用gl.enable(gl.POLYGON_OFFSET_FILL)开启多边形偏移功能
然后,利用gl.polygonOffset(1.0, 1.0)来指定计算偏移量的参数

4. 绘制立方体

要绘制立方体,仍然可以使用gl.drawArrays(),利用缓冲区数据来绘制表面,除此之外,为了高效利用图形中的坐标信息,可以使用gl.drawElements()配合将序列放到gl.ELEMENT_BUFFER_ARRAY来绘制表面。
具体做法如下:

function initPoint(gl, a_Position, a_Color) {
    let pointData = new Float32Array([
        0.0, 0.5, 0.0, 1.0, 0.0, 0.0,
        -0.5, -0.5, 0.5, 1.0, 0.0, 0.0,
        0.5, -0.5, 0.5, 1.0, 0.0, 0.0,

        0.0, 0.5, 0.0, 0.0, 1.0, 0.0,
        0.5, -0.5, 0.5, 0.0, 1.0, 0.0,
        0.0, -0.5, -0.5, 0.0, 1.0, 0.0,

        0.0, 0.5, 0.0, 0.0, 0.0, 1.0,
        0.0, -0.5, -0.5, 0.0, 0.0, 1.0,
        -0.5, -0.5, 0.5 ,0.0, 0.0, 1.0,

        -0.5, -0.5, 0.5, 1.0, 0.0, 1.0,
        0.0, -0.5, -0.5, 1.0, 0.0, 1.0,
        0.5, -0.5, 0.5, 1.0, 0.0, 1.0
    ]);
    // 利用的坐标序列
    let indexData = new Uint8Array([
        0, 1, 2,
        3, 4, 5,
        6, 7, 8,
        9, 10, 11
    ])
    let FSIZE = pointData.BYTES_PER_ELEMENT;
    let buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 6*FSIZE, 0);
    gl.enableVertexAttribArray(a_Position);
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, 6*FSIZE, 3*FSIZE);
    gl.enableVertexAttribArray(a_Color);
    // 将坐标序列信息存储到ELEMENT_ARRAY_BUFFER
    let indexBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexData, gl.STATIC_DRAW);
}

5. 总结

构建三维图形相比二维图形来说,需要使用视图转换和可视范围的矩阵信息,因此坐标信息就变为了:gl_Position = 可视矩阵 X 视图矩阵 X 变换矩阵 X 原始坐标,同时要利用深度规则消除深度影响,最后可以利用gl.drawElements(),定义序列,复用坐标信息

6. 参考

《WebGL编程指南》

你可能感兴趣的:(WebGL-学习笔记(四))