WebGL编程指南之进入三维世界

前言

前面介绍的都是二维图形,本文将从以下几个方面介绍三维图形:

  1. 以用户视角进入三维世界
  2. 控制二维可视空间
  3. 裁剪
  4. 处理物体的前后关系
  5. 绘制三维的立方体

1 立方体由三角形构成

三维物体也是由二维图形组成的,如下图所示:

既然三维物体是由三角形构成的,那么只需要逐个绘制组成物体的三角形就可以了,但是二维和三维还有一个显著的区别就是:二维只需要考虑x和y轴坐标,而绘制三维图形还要考虑他们的深度信息。

2 视点和视线

三维和二维图形最显著的区别就是,三维物体具有深度,也就是Z轴。事实上,我们所看到的三维图形其实是用户在特定视角上看到的有三维图形转化过来的二维图形,既绘制观察者看到的世界,而观察者可以在任意位置视察,定义一个观察者,需要考虑以下两点:

  1. 观察方向,既观察者自己在什么位置,在看场景的那一部分?
  2. 可是距离,既观察者能够看多远?

我们将观察者所处的位置称为视点(eye point) ,从视点出发沿着观察方向的射线称作视线(viewing direction)

2.1 视点、观察目标点和上方向

为了确定观察者的状态,你需要获取两项信息:视点 ,既观察者位置;观察目标点 ,既被观察目标所在的点,它可以用来确定视线。此外,我们需要把观察到的景象绘制到屏幕上,还需要直到上方向(up direction)。有了这三项信息就可以确定观察者状态了。

视点:观察者所在的三维空间位置,视线的起点

观察目标点:被观察目标所在的点。视线从视点出发,穿过观察目标点并继续延申。

上方向:最终绘制在屏幕上的影像中的向上的方向。试想,如果仅仅只是确定了视点和观察点,观察者还是可能以视线为轴旋转的(头部偏移会导致观察到的场景也发生偏移)。所以需要确定上方向。上方向是具有3个分量的矢量,用(upX,upY,upZ)表示。

我们可以用这三个矢量创建一个视图矩阵(view matrix),然后将该矩阵传给顶点着色器。视图矩阵可以表示观察者的状态,含有观察者的视点、管擦目标点、上方向等信息。

在WebGL中,观察者的默认状态如下:

  1. 视点位于坐标系统原点(0,0,0)
  2. 视线位于y轴负方向,观察点(0,0,-1),上方向是y轴正方向(0,1,0),如果将上方向改为X轴正半轴方向(1,0,0),你将看到场景旋转90度。

关键之处在于如何根据视图矩阵去计算场景图,如何将其应用到着色器。除了视图矩阵之外,在三维世界中还有模型矩阵和投影矩阵:

在三维图形学中,模型矩阵、视图矩阵和投影矩阵是用于将三维场景转换为二维屏幕坐标的重要工具,是三维图形变换的基础,分别负责物体的局部变换、相机视角和投影方式。通过组合这些矩阵,可以实现三维场景的正确渲染。它们各自的作用如下:

1. 模型矩阵 (Model Matrix)

  • 定义: 模型矩阵用于将物体的局部坐标系转换到世界坐标系。
  • 功能: 负责物体的平移、旋转和缩放。每个物体都有自己的模型矩阵,描述其在世界中的位置和方向。
  • 应用: 通过模型矩阵,可以将物体的顶点坐标从物体空间转换到世界空间。

2. 视图矩阵 (View Matrix)

  • 定义: 视图矩阵用于将场景从世界坐标系转换到视图坐标系。
  • 功能: 描述相机的位置和方向。视图矩阵可以看作是相机的变换矩阵,控制摄像机在场景中的观察角度和位置。视图矩阵是作用于整个三维模型,相当于三维大模型的模型矩阵。
  • 应用: 通过视图矩阵,可以调整观察场景的视角,从而影响渲染效果。

3. 投影矩阵 (Projection Matrix)

  • 定义: 投影矩阵用于将视图坐标系中的三维坐标转换为标准设备坐标(裁剪坐标,屏幕实际上相当于观察世界的窗口)。
  • 功能 : 负责定义视锥体(即可见的场景区域),通常有透视投影和正交投影两种类型。
    • 透视投影: 模拟人眼的视角,物体远离相机时会变小。
    • 正交投影: 物体的大小不随距离变化,适合二维图形。
  • 应用: 通过投影矩阵,可以将三维场景中的点转换为屏幕上的二维坐标。

矩阵变换顺序

在渲染过程中,这些矩阵通常按以下顺序应用M->V->P:

  1. 模型矩阵: 先将物体的顶点从局部空间变换到世界空间。
  2. 视图矩阵: 将物体在世界空间中的位置转换到相机视图空间。
  3. 投影矩阵: 将视图空间中的坐标转换为裁剪空间,准备进行屏幕映射。

其实根据自定义的观察者状态,绘制观察者看到的景象,与使用默认的观察状态,但是对三维对象进行平移、旋转和变换,再绘制观察者看到的景象,这两种行为是等价的,这也就是为什么视图矩阵相当于三维整体对象的模型矩阵。

比如,默认情况下,视线沿着Z轴负方向进行观察,假设我们将视点移动到(0,0,1),这时,视点与被观察到的三角形在Z轴上的距离增加了1.0个单位。实际上,这相当于我们使三角形沿着Z轴负方向移动1.0个单位,这两种方式可以达到同样的效果。既移动视点与移动被观察对象等效。

2.2 利用键盘改变视点

如果键盘右方向键按下,视点的X坐标将增大0.01;如果左方向键被按下,视点的X坐标减少0.01.

代码

matrix

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    #webgl {
      width: 600px;
      height: 600px;
      position: absolute;
      top: calc(50% - 300px);
      left: calc(50% - 300px);
      background-color: black;
    }
  </style>
  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./matrix.js"></script>
    <script>
      // 顶点着色器
      var VSHADER_SOURCE =
        "attribute vec4 a_Position;\n" +
        "attribute vec4 a_Color;\n" +
        "uniform mat4 u_ViewMatrix;\n" +
        "uniform mat4 u_ProjMatrix;\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n" +
        "  v_Color = a_Color;\n" +
        "}\n";

      // 片元这着色器
      var FSHADER_SOURCE =
        "#ifdef GL_ES\n" +
        "precision mediump float;\n" +
        "#endif\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_FragColor = v_Color;\n" +
        "}\n";

      function createProgram(gl, vshader, fshader) {
        var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
        var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
        return program;
      }

      function initShaders(gl, vshader, fshader) {
        var program = createProgram(gl, vshader, fshader);
        gl.useProgram(program);
        gl.program = program;
        return true;
      }

      function loadShader(gl, type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        return shader;
      }

      function main() {
        var canvas = document.getElementById("webgl");
        const gl = canvas.getContext("webgl");

        // 初始化着色器
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 初始化buffer
        var n = initVertexBuffers(gl);

        // 清除canvas
        gl.clearColor(0.0, 0.0, 0.0, 1.0);

        // 获取视图与模型矩阵存储位置
        var u_ViewMatrix = gl.getUniformLocation(gl.program, "u_ViewMatrix");
        var u_ProjMatrix = gl.getUniformLocation(gl.program, "u_ProjMatrix");

        //   视图矩阵
        var viewMatrix = new Matrix4();

        document.onkeydown = function (ev) {
          keydown(ev, gl, n, u_ViewMatrix, viewMatrix);
        };

        // 投影矩阵
        var projMatrix = new Matrix4();
        // 设置正交投影
        projMatrix.setOrtho(-1.0, 1.0, -1.0, 1.0, 0.0, 2.0);
        gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

        draw(gl, n, u_ViewMatrix, viewMatrix);
      }

      function initVertexBuffers(gl) {
        // 顶点和颜色
        var verticesColors = new Float32Array([
          0.0,
          0.5,
          -0.4,
          0.4,
          1.0,
          0.4, // 绿色三角形
          -0.5,
          -0.5,
          -0.4,
          0.4,
          1.0,
          0.4,
          0.5,
          -0.5,
          -0.4,
          1.0,
          0.4,
          0.4,

          0.5,
          0.4,
          -0.2,
          1.0,
          0.4,
          0.4, // 黄色三角形
          -0.5,
          0.4,
          -0.2,
          1.0,
          1.0,
          0.4,
          0.0,
          -0.6,
          -0.2,
          1.0,
          1.0,
          0.4,

          0.0,
          0.5,
          0.0,
          0.4,
          0.4,
          1.0, // 蓝色三角形
          -0.5,
          -0.5,
          0.0,
          0.4,
          0.4,
          1.0,
          0.5,
          -0.5,
          0.0,
          1.0,
          0.4,
          0.4,
        ]);
        var n = 9;

        var vertexColorbuffer = gl.createBuffer();

        gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
        gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

        var FSIZE = verticesColors.BYTES_PER_ELEMENT;

        var a_Position = gl.getAttribLocation(gl.program, "a_Position");
        gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
        gl.enableVertexAttribArray(a_Position);
        var a_Color = gl.getAttribLocation(gl.program, "a_Color");
        gl.vertexAttribPointer(
          a_Color,
          3,
          gl.FLOAT,
          false,
          FSIZE * 6,
          FSIZE * 3
        );
        gl.enableVertexAttribArray(a_Color);

        return n;
      }

      var g_EyeX = 0.2,
        g_EyeY = 0.25,
        g_EyeZ = 0.25; // 视点
      function keydown(ev, gl, n, u_ViewMatrix, viewMatrix) {
        if (ev.keyCode == 39) {
          g_EyeX += 0.01;
        } else if (ev.keyCode == 37) {
          g_EyeX -= 0.01;
        } else {
          return;
        }
        draw(gl, n, u_ViewMatrix, viewMatrix);
      }

      function draw(gl, n, u_ViewMatrix, viewMatrix) {
        // 设置视点
        viewMatrix.setLookAt(g_EyeX, g_EyeY, g_EyeZ, 0, 0, 0, 0, 1, 0);
        gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);

        //将缓冲区清除为预设值
        gl.clear(gl.COLOR_BUFFER_BIT);

        // 绘制三角形
        gl.drawArrays(gl.TRIANGLES, 0, n);
      }
    </script>
  </body>
</html>

效果图

其中涉及到了视图矩阵的计算,代码如下:

javascript 复制代码
/ * *
  *设置查看矩阵。
  * @param eyeX, eyeY, eyeZ眼点的位置。
    * @param centerX, centerY, centerZ参考点的位置。
    * @param upX, upY, upZ向上矢量的方向。
    * @return this
    * /
    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(z) and up(y) 叉乘(外积)的意义就是获取垂直于平面的法向量.
      sx = fy * upZ - fz * upY;
      sy = fz * upX - fx * upZ;
      sz = fx * upY - fy * upX;

      // Normalize s. 相机X基准
      rls = 1 / Math.sqrt(sx * sx + sy * sy + sz * sz);
      sx *= rls;
      sy *= rls;
      sz *= rls;

      // Calculate cross product of s and f. 相机y轴基准
      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);
    };

计算过程:

  1. 确定相机的位置和方向,包括视点,参考点和上方向(这三个可以确定相机坐标系)
  2. 计算视向向量(确定Z轴基准)
  3. 计算右向量 (确定X轴基准)
  4. 从新计算上向量 (确定Y轴基准,从新计算的原因是要确定坐标系统是正交的)

3 可视范围

事实上,在WebGL中,只会绘制在可视范围之内的图形,绘制可视范围之外的图形没有任何意义,即使绘制出来,他们也不会在屏幕上显示,,从某种程度上来讲,这样做模拟了人类观察物体的方式。除了水平和垂直范围内的限制,WebGL还限制观察者的可视深度,既能够看多远,所有这些限制包括水平视角、垂直视角和可视深度,定义了可视空间(view volume)

3.1 可视空间(正交投影)

有两类常用的可视空间:

  1. 长方体可视空间,也称盒状空间,由**正射投影(orthographic projection)**产生。
  2. 四棱锥/金字塔可视空间,由**透视投影(perspective projection)**产生。

透视投影下,产生的三维场景看上去更是有深度感,更加自然,应为我们平时观察真实世界用的也是透视投影。

盒状可视空间

盒状可视空间由前后两个矩形表面确定,分别称为近裁平面(near clipping plane)远裁平面(far clipping plane)。canvas上显示的就是可视空间中物体在近裁平面上的投影,如果裁剪面的宽高比跟canvas不一样,就会根据canvas的宽高比进行压缩,物体就会扭曲。近裁平面与远裁平面之间的盒状空间就是可视空间,只有在可视空间的物体才是可显示的。

3.2 定义盒状可视空间

定义盒状可视空间需要正交投影矩阵。正交投影的做法就是把物体移到原点处的 [-1,1]^3 的标准立方体中。

步骤分为两步:\ ① 先将物体中心平移到原点。\ ②再进行缩放,缩放到[-1,1]^3 的标准立方体中。

代码实现

Matrix

javascript 复制代码
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;
};

3.2 可视空间(透视投影)

正交投影无论远近,看到的图形大小不会改变,这通常与建筑平面图等技术绘图相吻合。透视投影赋予了深度概念,图形自适应与视点的距离。

上图描述了透视投影空间。就像盒状可视空间那样,透视投影可视空间也有视点、视线、近裁平面和远裁平面,这样在可视空间内的物体才会被显示出来。无论是透视投影空间还是盒状投影空间,都用投影矩阵表示。

3.2.1 变换矩阵推导

1、锥体变换成长方体

2、长方体进行正交投影到[-1,1]^3 的标准立方体中

首先我们观察在变换的过程中有以下几个特点:\ 1、变换后近平面的任何点都不变\ 2、变换后远平面的z不变(但是并不是说在近平面和远平面之间的平面z的值不会变)\ 3、变换后远平面的中心点不会变

椎体换成长方体

根据相似三角形可得变换后的x和y坐标。

对于齐次坐标,各个维度乘以相同的值得到的点和原来一样。

可得变换矩阵具备以下形式

用n代替z,表示近平面上的点。经过变换矩阵之后,要满足上面提到的两个条件:即,任何在近平面上的点,经过变换之后不变,任何远平面上的点经过变换之后Z不变,只是x和y轴的挤压。

变换矩阵第三行满足以下等式****

这是齐次坐标表示,变换矩阵的第四行第三列为1,齐次坐标第四个分量采用z表示,所以上面是n^2^。

最后左乘正交投影矩阵可得完整的透视投影矩阵:

在近平面和远平面之间的点Z值会偏小。

3.2.2 定义透视投影矩阵

参数

fov 指定垂直视角,即可视空间侧面和底面间的夹角,必须大于0
aspect 指定近裁面的宽高比
near、far 指定近裁面和远裁面的位置,即可视空间的近边界和远边界,必须大于0

代码实现

javascript 复制代码
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.2.3 代码示例
html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    #webgl {
      width: 600px;
      height: 600px;
      position: absolute;
      top: calc(50% - 300px);
      left: calc(50% - 300px);
      background-color: black;
    }
  </style>
  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./matrix.js"></script>
    <script>
      // 顶点着色器
      var VSHADER_SOURCE =
        "attribute vec4 a_Position;\n" +
        "attribute vec4 a_Color;\n" +
        "uniform mat4 u_ViewMatrix;\n" +
        "uniform mat4 u_ProjMatrix;\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n" +
        "  v_Color = a_Color;\n" +
        "}\n";

      // 片元着色器
      var FSHADER_SOURCE =
        "#ifdef GL_ES\n" +
        "precision mediump float;\n" +
        "#endif\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_FragColor = v_Color;\n" +
        "}\n";

      function createProgram(gl, vshader, fshader) {
        var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
        var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
        return program;
      }

      function initShaders(gl, vshader, fshader) {
        var program = createProgram(gl, vshader, fshader);
        gl.useProgram(program);
        gl.program = program;
        return true;
      }

      function loadShader(gl, type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        return shader;
      }

      function main() {
        var canvas = document.getElementById("webgl");
        const gl = canvas.getContext("webgl");

        // 初始化着色器
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 初始化buffer
        var n = initVertexBuffers(gl);

        // 清除canvas
        gl.clearColor(0.0, 0.0, 0.0, 1.0);

        // 获取视图与模型矩阵存储位置
        var u_ViewMatrix = gl.getUniformLocation(gl.program, "u_ViewMatrix");
        var u_ProjMatrix = gl.getUniformLocation(gl.program, "u_ProjMatrix");

        //   视图矩阵
        var viewMatrix = new Matrix4();
        // 投影矩阵
        var projMatrix = new Matrix4();
        viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);
        projMatrix.setPerspective(30, canvas.width / canvas.height, 1, 100);
        gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
        gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

        gl.clear(gl.COLOR_BUFFER_BIT);

        gl.drawArrays(gl.TRIANGLES, 0, n);
      }

      function initVertexBuffers(gl) {
        // 顶点和颜色
        var verticesColors = new Float32Array([
          // Three triangles on the right side
          0.75,
          1.0,
          -4.0,
          0.4,
          1.0,
          0.4, // The back green one
          0.25,
          -1.0,
          -4.0,
          0.4,
          1.0,
          0.4,
          1.25,
          -1.0,
          -4.0,
          1.0,
          0.4,
          0.4,

          0.75,
          1.0,
          -2.0,
          1.0,
          1.0,
          0.4, // The middle yellow one
          0.25,
          -1.0,
          -2.0,
          1.0,
          1.0,
          0.4,
          1.25,
          -1.0,
          -2.0,
          1.0,
          0.4,
          0.4,

          0.75,
          1.0,
          0.0,
          0.4,
          0.4,
          1.0, // The front blue one
          0.25,
          -1.0,
          0.0,
          0.4,
          0.4,
          1.0,
          1.25,
          -1.0,
          0.0,
          1.0,
          0.4,
          0.4,

          // Three triangles on the left side
          -0.75,
          1.0,
          -4.0,
          0.4,
          1.0,
          0.4, // The back green one
          -1.25,
          -1.0,
          -4.0,
          0.4,
          1.0,
          0.4,
          -0.25,
          -1.0,
          -4.0,
          1.0,
          0.4,
          0.4,

          -0.75,
          1.0,
          -2.0,
          1.0,
          1.0,
          0.4, // The middle yellow one
          -1.25,
          -1.0,
          -2.0,
          1.0,
          1.0,
          0.4,
          -0.25,
          -1.0,
          -2.0,
          1.0,
          0.4,
          0.4,

          -0.75,
          1.0,
          0.0,
          0.4,
          0.4,
          1.0, // The front blue one
          -1.25,
          -1.0,
          0.0,
          0.4,
          0.4,
          1.0,
          -0.25,
          -1.0,
          0.0,
          1.0,
          0.4,
          0.4,
        ]);
        var n = 18;

        var vertexColorbuffer = gl.createBuffer();

        gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
        gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

        var FSIZE = verticesColors.BYTES_PER_ELEMENT;

        var a_Position = gl.getAttribLocation(gl.program, "a_Position");
        gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
        gl.enableVertexAttribArray(a_Position);
        var a_Color = gl.getAttribLocation(gl.program, "a_Color");
        gl.vertexAttribPointer(
          a_Color,
          3,
          gl.FLOAT,
          false,
          FSIZE * 6,
          FSIZE * 3
        );
        gl.enableVertexAttribArray(a_Color);

        return n;
      }
    </script>
  </body>
</html>

预览

现象描述

根据上图所示,距离较远的三角形看上去变小了,其次,三角形被不同程度的平移以贴近中心线,使得他们看上去左右排成了两列。实际上这些三角形大小是完全相同的,透视投影矩阵对三角形进行了缩放变换:

根据三角形与视点的距离,按比例对三角形进行了缩小变换


在近平面和远平面之间的平面经过Mpersp->ortho变换后z的值会如何变化?变大?变小?不变?

通过投影矩阵可得z的函数:

markdown 复制代码
f(n) = n+f - nf/z
//定义比较表达式
n+f - nf/z - z > 0
// 两侧乘以z
z(n + f) - nf  - z * z > 0
// 定义函数
f(z) = z(n + f) - nf - z*z
// 我们知道,变换矩阵在近平面和远平面z值不变,这里只关注近平面与远平面之间的z值变化情况,对f(z)求导
g(z) = n + f - 2*z
// n < z , z < f 可知,导数大于0,可知 在n到f之间 f(z) > z

z在n到f之间f(n)是大于z的,所以变换后的z值会偏大。

4 正确处理对象的前后关系

上面示例定义顶点数据时都是先定义远处顶点,在定义近处顶点,WebGL是按照顶点在缓冲区中的顺序来进行绘制的,先绘制远处的,在绘制近处的,后悔之的图形会覆盖已经绘制好的图形,这样就产生了覆盖效果。如果场景中的对象不发生运动,观察者的状态也是唯一的,那么这种做法没有问题,但是如果你希望不断移动视点,从不同角度看物体,那么你不可能事先决定对象出现的顺序。

隐藏面消除

为了解决这个问题,WebGL提供了**隐藏面消除(hidden suaface removal)**功能。这个功能会帮助我们消除那些被遮挡的表面(隐藏面),你可以放心绘制场景而不必顾及各物体在缓冲区中的顺序,因为那些远处的物体会自动被近处的物体遮挡,不会被绘制出来。

开启隐藏面消除功能,需要遵循以下两步:

1、开启隐藏面消除功能

gl.enable(gl.DEPTH_TEST)

2、在绘制之前,清除深度缓冲区

gl.clear(gl.DEPTH_BUFFER_BIT)

绘制之前还需要清除颜色缓冲区

gl.clear(gl.COLOR_BUFFER_BIT)

5 深度冲突

当几何图形的两个表面极为接近时,就会出现新的问题,使得表面上看上去斑斑驳驳的,如下图所示:

产生深度冲突的原因是因为两个表面过于接近,深度缓冲区有限的精度已经不能区分那个在前,那个在后。WebGL提供了一种被称为**多边形偏移(polygon offest)**的机制来解决这个问题。该机制会自动在Z值加上一个偏移量,偏移量的值由物体表面相对于观察者视线的角度来确定,启动该机制只需要两行代码:

1、启动多边形偏移

gl.enable(gl.POLYGON_OFFEST_FILL)

2、在绘制之前指定用来计算偏移量的参数

gl.golyonOffest(1.0, 1.0)

golyonOffest指定加到每个顶点绘制后z值上的偏移量,偏移量按照公式m * factor + r * units计算,其中m表示顶点所在表面相对于观察者的视线的角度,而r表示硬件能够区分两个z值之差的最小值。

6 立方体

前面都是利用gl.drawArrays来绘制图形,如果我们想要绘制一个立方体呢?当然可以通过两个三角形来绘制一个立方体表面,但是这样就会定义24个顶点,而且要调用6次gl.drawArrays。WebGL提供了一种完美的方案:gl.drawElements()。gl.drawElements()能够避免重复定义顶点,保持顶点数量最小,立方体只需要8个顶点,为此需要知道模型的每一个顶点坐标,这些顶点坐标描述了整个模型。

gl.drawElements()参数描述:

mode 指定绘制方式,可以接受以下常量符号:gl.POINTS、gl.LINES、gl.LINE_STRIP、gl.LINE_LOOP、gl.TRIANGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN
count 指定绘制顶点个数
type 指定索引值数据类型:gl.UNSIGN_BYTE 、gl.UNSIGNED_SHORT
offest 指定所以数组中开始绘制的位置

我们需要在gl.ELEMENT_ARRAY_BUFFER(而不是之前的gl.ARRAY_BUFFER)指定顶点的索引值,我们需要根据索引值来指定顶点的获取顺序,获取方式如下图:

代码示例

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    #webgl {
      width: 600px;
      height: 600px;
      position: absolute;
      top: calc(50% - 300px);
      left: calc(50% - 300px);
      background-color: black;
    }
  </style>
  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./matrix.js"></script>
    <script>
      // 顶点着色器
      var VSHADER_SOURCE =
        "attribute vec4 a_Position;\n" +
        "attribute vec4 a_Color;\n" +
        "uniform mat4 u_MvpMatrix;\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_Position = u_MvpMatrix * a_Position;\n" +
        "  v_Color = a_Color;\n" +
        "}\n";

      // 片元着色器
      var FSHADER_SOURCE =
        "#ifdef GL_ES\n" +
        "precision mediump float;\n" +
        "#endif\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_FragColor = v_Color;\n" +
        "}\n";

      function createProgram(gl, vshader, fshader) {
        var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
        var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
        return program;
      }

      function initShaders(gl, vshader, fshader) {
        var program = createProgram(gl, vshader, fshader);
        gl.useProgram(program);
        gl.program = program;
        return true;
      }

      function loadShader(gl, type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        return shader;
      }

      function main() {
        var canvas = document.getElementById("webgl");
        const gl = canvas.getContext("webgl");

        // 初始化着色器
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 初始化buffer
        var n = initVertexBuffers(gl);

        // 清除canvas
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.enable(gl.DEPTH_TEST);
        var u_MvpMatrix = gl.getUniformLocation(gl.program, "u_MvpMatrix");
        var mvpMatrix = new Matrix4();
        mvpMatrix.setPerspective(30, 1, 1, 100);
        mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
        gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
      }

      function initVertexBuffers(gl) {
        // Create a cube
        //    v6----- v5
        //   /|      /|
        //  v1------v0|
        //  | |     | |
        //  | |v7---|-|v4
        //  |/      |/
        //  v2------v3
        var verticesColors = new Float32Array([
          // 顶点坐标和颜色
          1.0,  1.0,  1.0,     1.0,  1.0,  1.0,  // v0 White
          -1.0,  1.0,  1.0,     1.0,  0.0,  1.0,  // v1 Magenta
          -1.0, -1.0,  1.0,     1.0,  0.0,  0.0,  // v2 Red
          1.0, -1.0,  1.0,     1.0,  1.0,  0.0,  // v3 Yellow
          1.0, -1.0, -1.0,     0.0,  1.0,  0.0,  // v4 Green
          1.0,  1.0, -1.0,     0.0,  1.0,  1.0,  // v5 Cyan
          -1.0,  1.0, -1.0,     0.0,  0.0,  1.0,  // v6 Blue
          -1.0, -1.0, -1.0,     0.0,  0.0,  0.0   // v7 Black
        ]);

        var indices = new Uint8Array([
            0, 1, 2,   0, 2, 3,    // front
            0, 3, 4,   0, 4, 5,    // right
            0, 5, 6,   0, 6, 1,    // up
            1, 6, 7,   1, 7, 2,    // left
            7, 4, 3,   7, 3, 2,    // down
            4, 7, 6,   4, 6, 5     // back
        ]);

        var vertexColorbuffer = gl.createBuffer();
        var indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
        gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
        var FSIZE = verticesColors.BYTES_PER_ELEMENT;
        var a_Position = gl.getAttribLocation(gl.program, "a_Position");
        gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
        gl.enableVertexAttribArray(a_Position);
        var a_Color = gl.getAttribLocation(gl.program, "a_Color");
        gl.vertexAttribPointer(
          a_Color,
          3,
          gl.FLOAT,
          false,
          FSIZE * 6,
          FSIZE * 3
        );
        gl.enableVertexAttribArray(a_Color);
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

        return indices.length;
      }
    </script>
  </body>
</html>

效果图

6.1 为立方体的每个表面指定颜色

顶点着色器进行的是逐顶点计算,接受的是逐顶点的信息。这说明你想要指定表面的颜色,也需要将颜色定义为逐顶点的信息,并传给顶点着色器。问题在于如果设置同一面的顶点颜色为相同值,但是这个顶点都会被其它共用,为了解决这个问题,需要创建多个相同顶点坐标的顶点,并为顶点和颜色分别创建buffer:处理过程如下图所示:

代码示例

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    #webgl {
      width: 600px;
      height: 600px;
      position: absolute;
      top: calc(50% - 300px);
      left: calc(50% - 300px);
      background-color: black;
    }
  </style>
  <body onload="main()">
    <canvas id="webgl" width="600" height="600">
      Please use a browser that supports "canvas"
    </canvas>
    <script src="./matrix.js"></script>
    <script>
      // 顶点着色器
      var VSHADER_SOURCE =
        "attribute vec4 a_Position;\n" +
        "attribute vec4 a_Color;\n" +
        "uniform mat4 u_MvpMatrix;\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_Position = u_MvpMatrix * a_Position;\n" +
        "  v_Color = a_Color;\n" +
        "}\n";

      // 片元着色器
      var FSHADER_SOURCE =
        "#ifdef GL_ES\n" +
        "precision mediump float;\n" +
        "#endif\n" +
        "varying vec4 v_Color;\n" +
        "void main() {\n" +
        "  gl_FragColor = v_Color;\n" +
        "}\n";

      function createProgram(gl, vshader, fshader) {
        var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
        var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
        var program = gl.createProgram();
        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);
        gl.linkProgram(program);
        var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
        return program;
      }

      function initShaders(gl, vshader, fshader) {
        var program = createProgram(gl, vshader, fshader);
        gl.useProgram(program);
        gl.program = program;
        return true;
      }

      function loadShader(gl, type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
        return shader;
      }

      function main() {
        var canvas = document.getElementById("webgl");
        const gl = canvas.getContext("webgl");

        // 初始化着色器
        initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE);

        // 初始化buffer
        var n = initVertexBuffers(gl);

        // 清除canvas
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.enable(gl.DEPTH_TEST);
        var u_MvpMatrix = gl.getUniformLocation(gl.program, "u_MvpMatrix");
        var mvpMatrix = new Matrix4();
        mvpMatrix.setPerspective(30, 1, 1, 100);
        mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
        gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

        gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
      }

      function initVertexBuffers(gl) {
        // Create a cube
        //    v6----- v5
        //   /|      /|
        //  v1------v0|
        //  | |     | |
        //  | |v7---|-|v4
        //  |/      |/
        //  v2------v3
        var vertices = new Float32Array([   // Vertex coordinates
          1.0, 1.0, 1.0,  -1.0, 1.0, 1.0,  -1.0,-1.0, 1.0,   1.0,-1.0, 1.0,  // v0-v1-v2-v3 front
          1.0, 1.0, 1.0,   1.0,-1.0, 1.0,   1.0,-1.0,-1.0,   1.0, 1.0,-1.0,  // v0-v3-v4-v5 right
          1.0, 1.0, 1.0,   1.0, 1.0,-1.0,  -1.0, 1.0,-1.0,  -1.0, 1.0, 1.0,  // v0-v5-v6-v1 up
          -1.0, 1.0, 1.0,  -1.0, 1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0,-1.0, 1.0,  // v1-v6-v7-v2 left
          -1.0,-1.0,-1.0,   1.0,-1.0,-1.0,   1.0,-1.0, 1.0,  -1.0,-1.0, 1.0,  // v7-v4-v3-v2 down
          1.0,-1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0, 1.0,-1.0,   1.0, 1.0,-1.0   // v4-v7-v6-v5 back
        ]);

        var colors = new Float32Array([     // Colors
          0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  // v0-v1-v2-v3 front(blue)
          0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  // v0-v3-v4-v5 right(green)
          1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  // v0-v5-v6-v1 up(red)
          1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  // v1-v6-v7-v2 left
          1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  // v7-v4-v3-v2 down
          0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0   // v4-v7-v6-v5 back
        ]);

        var indices = new Uint8Array([       // Indices of the vertices
          0, 1, 2,   0, 2, 3,    // front
          4, 5, 6,   4, 6, 7,    // right
          8, 9,10,   8,10,11,    // up
          12,13,14,  12,14,15,    // left
          16,17,18,  16,18,19,    // down
          20,21,22,  20,22,23     // back
        ]);
        initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position')
        initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color')
        // 索引buffer
        var indexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
        gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

        return indices.length;
      }

      function initArrayBuffer(gl, data, num, type, attribute) {
        var buffer = gl.createBuffer();   
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
        var a_attribute = gl.getAttribLocation(gl.program, attribute);
        gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
        gl.enableVertexAttribArray(a_attribute);
        return true;
      }
    </script>
  </body>
</html>

参考文献

现代计算机图形学(正交投影,透视投影,MVP变换)-CSDN博客

Lecture 04 Transformation Cont._哔哩哔哩_bilibili

图形学投影矩阵推导_哔哩哔哩_bilibili

相关推荐
时清云31 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学39 分钟前
宏队列和微队列
前端·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋2 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者4 小时前
React 19 新特性详解
前端
小程xy4 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6324 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
寻找09之夏5 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js