webgl入门(三): 使用webgl绘制网格

448 阅读3分钟

在webgl使用的坐标系如下图所示:

20210505093306

并且只有在标准立方体[1,1]3[-1,1]^3范围内物体才会显示在屏幕中.

如果只考虑二维的情况, 坐标系则如下图所示:

20210505093602

我们可以用两个三角形覆盖整个画布:

20210505094449

我们要绘制的4x4网格, 如下图所示:

20210505095619

我们将关注点先放在一个小的单元:

20210505095931

小的单元单独显示如下图:

20210505100132

更进一步, 我将像素也画出来, 其中每个小方格为一个像素:

20210505115939

我们的目标是决定每个像素的颜色, 到底是白色还是灰色. 这个工作在webgl中肯定是在片元着色器中完成.

那判断是是白色还是灰色的依据又是什么?

当然是根据像素的位置信息, 已知画布范围[1,1]2[-1, 1]^2, 并且每行有4个网格每列有4个网格, 那么每单个网格单元的大小为0.5 * 0.5. 假设网格线的宽度为单个网格单元大小的1/10. 我对上图进行了坐标标注:

20210505120005

如上图说所示, 在片元着色器中, 如果我们知道当前像素的坐标(x,y), 那么我们就可以判断出, 该像素是白色还是灰色.

直觉上我们应该这么做, 但是这里有两个问题:

  1. 如何在片元着色器中获取当前像素的坐标(x,y)
  2. 上图中我只标注了第一个网格单元的坐标信息, 可以想到, 每个网格单元的坐标信息都是不一样的, 这样我们写的片元着色器就无法适用于所有网格单元.

首先, 如何在片元着色器中获取当前像素的坐标?

我们可以通过线性插值的方式, 类似于获取纹理坐标的方式. 具体实现如下:

attributes: {
  position: [
    [-1, -1],
    [-1, 1],
    [1, 1],
    [1, -1],
  ],
  uv: [
    [-1, -1],
    [-1, 1],
    [1, 1],
    [1, -1],
  ],
},
// cell
elements: [[0,1,2], [2,0,3]]

顶点着色器:

attribute vec2 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = vec4(position, 0, 1);
}

片元着色器:

precision mediump float;
varying vec2 vUv;
void main () {
  vec2 st = vUv; // st.x st.y 就可以代表该像素的xy
}

关于问题2, 本质的原因是网格其实是周期性的, 所以我们需要将[-1, 1]这个线性的区间变成一个周期性的变化, 具体方法如下:

  1. 将原来的st.xy由[1,1]2[-1, 1]^2映射为[0,4]2[0, 4]^2:
attributes: {
  uv: [
    [0, 0],
    [0, 4],
    [4, 4],
    [4, 0],
  ],
}

如下图所示:

20210505115753

  1. 形成周期性变换, 可以用fract函数, 它的作用是只取小数部分
precision mediump float;
varying vec2 vUv;
void main () {
  vec2 st = fract(vUv);
}

vUv的取值为0-4之间, 这样就可以形成一个周期性的变换, 如下图所示:

20210505114507

  1. 下面看看完整的片元着色器
precision mediump float;
varying vec2 vUv;
void main () {
  vec2 st = fract(vUv);

  if(st.x > 0.9 || st.y < 0.1) {
    gl_FragColor.rgb = vec3(0.8); // 灰色
  } else {
    gl_FragColor.rgb = vec3(1); // 白色
  }
  gl_FragColor.a = 1.0;
}

我们还可以这么来写, 和上面的效果是等价的:

void main () {
  vec2 st = fract(vUv);
  float d1 = step(st.x, 0.9);
  float d2 = step(0.1, st.y);
  gl_FragColor.rgb = mix(vec3(0.8), vec3(1.0), d1 * d2);
  gl_FragColor.a = 1.0;
}

step的使用: 当 step(a, b) 中的 b < a 时,返回 0;当 b >= a 时,返回 1.

mix的使用: mix(a, b, c) 表示根据 c 是 0 或 1,返回 a 或者 b.

最后, 如果需要绘制16x16, 32x32的网格, 我们需要将uv改一下, 比如:


attributes: {
  uv: [
    [0, 0],
    [0, 16],
    [16, 16],
    [16, 0],
  ],
}

这样还是有点麻烦, 我们稍微调整一下: uv的范围我们设置成[0,1]2[0,1]^2, 然后在片元着色器中将vUv乘以16, 效果其实是一样的. 其实做的事情都是将区间[0,1]2[0,1]^2映射成了区间[0,16]2[0,16]^2.

uniforms: {
  rows: 16,
},
attributes: {
  uv: [
    [0, 0],
    [0, 1],
    [1, 1],
    [1, 0],
  ],
}
uniform float rows;
void main () {
  vec2 st = fract(vUv * rows);
  ...
}

在线demo