<audio id="audio" title="14 | 如何使用片元着色器进行几何造型？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/3f/4e80a2a0cb52e864f28d6ca9fa0c953f.mp3"></audio>

你好，我是月影。

在WebGL中，片元着色器有着非常强大的能力，它能够并行处理图片上的全部像素，让数以百万计的运算同时完成。但也正因为它是并行计算的，所以它和常规代码顺序执行或者串行执行过程并不一样。因此，在使用片元着色器实现某些功能的时候，我们要采用与常规的JavaScript代码不一样的思路。

到底哪里不一样呢？今天，我就通过颜色控制，以及线段、曲线、简单几何图形等的绘制，来讲讲片元着色器是怎么进行几何造型的，从而加深你对片元着色器绘图原理的理解。

首先，我们来说比较简单的颜色控制。

## 如何用片元着色器控制局部颜色？

我们知道，片元着色器能够用来控制像素颜色，最简单的就是把图片绘制为纯色。比如，通过下面的代码，我们就把一张图片绘制为了纯黑色。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  gl_FragColor = vec4(0, 0, 0, 1);
}

```

如果想让一张图片呈现不同的颜色，我们还可以根据纹理坐标值来绘制，比如，通过下面的代码，我们就可以让某个图案的颜色，从左到右由黑向白过渡。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  gl_FragColor.rgb = vec3(vUv.x);
  gl_FragColor.a = 1.0;
}

```

不过，这种颜色过渡还比较单一，这里我们还可以改变一下渲染方式让图形呈现的效果更复杂。比如说，我们可以使用乘法创造一个10*10的方格，让每个格子左上角是绿色，右下角是红色，中间是过渡色。代码和显示的效果如下所示：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  vec2 st = vUv * 10.0;
  gl_FragColor.rgb = vec3(fract(st), 0.0);
  gl_FragColor.a = 1.0;
}

```

<img src="https://static001.geekbang.org/resource/image/9b/f6/9b33fc3c5b08343114c479574f0484f6.jpeg" alt="">

不仅如此，我们还可以在上图的基础上继续做调整。我们可以通过idx = floor(st)获取网格的索引，判断网格索引除以2的余数（奇偶性），根据它来决定是否翻转网格内的x、y坐标。这样操作后的代码和图案如下所示：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  vec2 st = vUv * 10.0;
  vec2 idx = floor(st);
  vec2 grid = fract(st);

  vec2 t = mod(idx, 2.0);
  
  if(t.x == 1.0) {
    grid.x = 1.0 - grid.x;
  }
  if(t.y == 1.0) {
    grid.y = 1.0 - grid.y;
  }
  gl_FragColor.rgb = vec3(grid, 0.0);
  gl_FragColor.a = 1.0;
}

```

<img src="https://static001.geekbang.org/resource/image/66/a2/66b0826578525073320c14e6120a8aa2.jpeg" alt="">

事实上，再改用不同的方式，我们还可以生成更多有趣的图案。不过，这里我们就不继续了，因为上面这些做法有点像是灵机一动的小技巧。实际上，我们缺少的并不是小技巧，而是一套统一的方法论。我们希望能够利用它，在着色器里精确地绘制出我们想要的几何图形。

## 如何用片元着色器绘制圆、线段和几何图形

那接下来，我们就通过几个例子，把片元着色器精确绘图的方法论给总结出来。

### 1. 绘制圆

首先，我们从最简单的几何图形，也就是圆开始，来说说片元着色器的绘图过程。

一般来说，我们画圆的时候是根据点坐标到圆心的距离来生成颜色的。在片元着色器中，我们可以用distance函数求一下vUv和画布中点vec2(0.5)的距离，然后根据这个值设置颜色。代码如下：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  float d = distance(vUv, vec2(0.5));
  gl_FragColor.rgb = d * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

通过这样的方法，我们最终绘制出了一个模糊的圆，效果如下：

<img src="https://static001.geekbang.org/resource/image/84/58/84239f4aaf8f54cdbe93a65e3bfd2d58.jpeg" alt="">

为什么这个圆是模糊的呢？这是因为越靠近圆心，距离d的值越小， gl_FragColor.rgb = d * vec3(1.0); 的颜色值也就越接近于黑色。

那如果我们要实现一个更清晰的圆应该怎么做呢？这个时候，你别忘了还有step函数。我们用step函数基于0.2做阶梯，就能得到一个半径为0.2的圆。实现代码和最终效果如下：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  float d = distance(vUv, vec2(0.5));
  gl_FragColor.rgb = step(d, 0.2) * vec3(1.0);
  gl_FragColor.a = 1.0;
}


```

<img src="https://static001.geekbang.org/resource/image/8b/36/8b3acfbe004038caba9f9c1a1429ae36.jpeg" alt="">

不过，你会发现我们得到的这个圆的边缘很不光滑。这是因为浮点数计算的精度导致的锯齿现象。为了解决这个问题，我们用smoothstep代替step。

<img src="https://static001.geekbang.org/resource/image/3f/c8/3f73b8cf75cf0c67cdb7815d0b7051c8.jpeg" alt="">

为什么smoothstep代替step就可以得到比较光滑的圆呢？这是因为smoothstep和step类似，都是阶梯函数。但是，与step的值是直接跳跃的不同，smoothstep在step-start和step-end之间有一个平滑过渡的区间。因此，用smoothstep绘制的圆，边缘就会有一圈颜色过渡，就能从视觉上消除锯齿。

<img src="https://static001.geekbang.org/resource/image/52/d7/52c7484a1e18526e2fca815c4083b5d7.jpeg" alt="">

片元着色器绘制的圆，在构建图像的粒子效果中比较常用。比如，我们可以用它来实现图片的渐显渐隐效果。下面是片元着色器中代码，以及我们最终能够实现的效果图。

```
#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D tMap;
uniform vec2 uResolution;
uniform float uTime;
varying vec2 vUv;

float random (vec2 st) {
    return fract(sin(dot(st.xy,
                        vec2(12.9898,78.233)))*
        43758.5453123);
}

void main() {
    vec2 uv = vUv;
    uv.y *= uResolution.y / uResolution.x;
    vec2 st = uv * 100.0;
    float d = distance(fract(st), vec2(0.5));
    float p = uTime + random(floor(st));
    float shading = 0.5 + 0.5 * sin(p);
    d = smoothstep(d, d + 0.01, 1.0 * shading);
    vec4 color = texture2D(tMap, vUv);
    gl_FragColor.rgb = color.rgb * clamp(0.5, 1.3, d + 1.0 * shading);
    gl_FragColor.a = color.a;
}

```

<img src="https://static001.geekbang.org/resource/image/a6/9d/a6ffc10e75f217078c68c8dd6a7b4f9d.gif" alt="">

### 2. 绘制线

利用片元着色器绘制圆的思路，就是根据点到圆心的距离来设置颜色。实际上，我们也可以用同样的原理来绘制线，只不过需要把点到点的距离换成点到直线（向量）的距离。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  vec3 line = vec3(1, 1, 0);
  float d = abs(cross(vec3(vUv,0), normalize(line)).z); 
  gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

比如，我们利用上面的代码，就能在画布上画出一条斜线。

<img src="https://static001.geekbang.org/resource/image/61/7f/618fc882f894386eccdcf4bdea233e7f.jpeg" alt="">

如果你还不能一眼看出上面的代码为什么能画出一条直线，说明你对于图形学的向量计算思维还没有完全适应。不过别着急，随着我们练习的增多，你会逐渐适应的。下面，我来解释一下这段代码。

这里，我们用一个三维向量line来定义一条直线。因为我们要绘制的是2D图形，所以z保持0就行，而x和y用来决定方向。

然后呢，我们求vUv和line的距离。这里我们直接用向量叉乘的性质就能求得。因为两个二维向量叉积的z轴分量的大小，就是这两个向量组成的平行四边形的面积，那当我们把line的向量归一化之后，这个值就是vUv到直线的距离d了。因为这个d带符号，所以我们还需要取它的绝对值。

最后，我们用这个d结合前面使用过的smoothstep来控制像素颜色，就能得到一条直线了。

### 3. 用鼠标控制直线

画出直线之后，我们改变line还可以得到不同的直线。比如，在着色器代码中，我们再添加一个uniform变量uMouse，就可以根据鼠标位置来控制直线方向。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;
uniform vec2 uMouse;

void main() {
  vec3 line = vec3(uMouse, 0); // 用向量表示所在直线
  float d = abs(cross(vec3(vUv,0), normalize(line)).z); // 叉乘表示平行四边形面积，底边为1，得到距离
  gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

对应地，我们需要在JavaScript中将uMouse通过uniforms传入，代码如下：

```
const canvas = document.querySelector('canvas');
const renderer = new GlRenderer(canvas);
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);

renderer.uniforms.uMouse = [-1, -1];

canvas.addEventListener('mousemove', (e) =&gt; {
  const {x, y, width, height} = e.target.getBoundingClientRect();
  renderer.uniforms.uMouse = [
    (e.x - x) / width,
    1.0 - (e.y - y) / height,
  ];
});

renderer.setMeshData([{
  positions: [
    [-1, -1],
    [-1, 1],
    [1, 1],
    [1, -1],
  ],
  attributes: {
    uv: [
      [0, 0],
      [0, 1],
      [1, 1],
      [1, 0],
    ],
  },
  cells: [[0, 1, 2], [2, 0, 3]],
}]);

renderer.render();

```

<img src="https://static001.geekbang.org/resource/image/aa/71/aa095666cfb32167aff86d051b929271.gif" alt="">

在上面的例子中，我们的直线是经过原点的。那如果我们想让直线经过任意的定点该怎么办？我们可以加一个uniform变量uOrigin，来表示直线经过的固定点。代码如下：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;
uniform vec2 uMouse;
uniform vec2 uOrigin;

void main() {
  vec3 line = vec3(uMouse - uOrigin, 0); // 用向量表示所在直线
  float d = abs(cross(vec3(vUv - uOrigin, 0), normalize(line)).z); // 叉乘表示平行四边形面积，底边为1，得到距离
  gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

延续这个绘制直线的思路，我们很容易就能知道该如何绘制线段了。绘制线段与绘制直线的方法几乎一样，只不过，我们要将计算点到直线的距离修改为计算点到线段的距离。

但是因为点和线段之间有两种关系，一种是点在线段上，另一种是在线段之外。所以我们在求点到线段的距离d的时候，要分两种情况讨论：当点到线段的投影位于线段两个端点中间的时候，它就等于点到直线的距离；当点到线段的投影在两个端点之外的时候，它就等于这个点到最近一个端点的距离。

这么说还是比较抽象，我画了一个示意图。你会看到，C<sub>1</sub>到线段ab的距离就等于它到线段所在直线的距离，C<sub>2</sub>到线段ab的距离是它到a点的距离，C<sub>3</sub>到线段的距离是它到b点的距离。那么如何判断究竟是C<sub>1</sub>、C<sub>2</sub>、C<sub>3</sub>中的哪一种情况呢？答案是通过C<sub>1</sub>到线段ab的投影来判断。

<img src="https://static001.geekbang.org/resource/image/64/2b/64cb19cd9fabb219db3d185d3a77922b.jpeg" alt="">

所以，我们在原本片元着色器代码的基础上，抽象出一个seg_distance函数，用来返回点到线段的距离。修改后的代码如下：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;
uniform vec2 uMouse;
uniform vec2 uOrigin;

float seg_distance(in vec2 st, in vec2 a, in vec2 b) {
  vec3 ab = vec3(b - a, 0);
  vec3 p = vec3(st - a, 0);
  float l = length(ab);
  float d = abs(cross(p, normalize(ab)).z);
  float proj = dot(p, ab) / l;
  if(proj &gt;= 0.0 &amp;&amp; proj &lt;= l) return d;
  return min(distance(st, a), distance(st, b));
}

void main() {
  float d = seg_distance(vUv, uOrigin, uMouse);
  gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

这么修改之后，如果我们将uOrigin设为vec2(0.5, 0.5)，就会得到如下效果：

<img src="https://static001.geekbang.org/resource/image/b4/06/b4b5cf71a53c50b95a7e4abbee40d506.gif" alt="">

### 4. 绘制三角形

你可能已经发现了，不管是画圆还是画线，我们使用的原理都是求点到点或者是点到线段距离。实际上，这个原理还可以扩展应用到封闭平面图形的绘制上。那我们就以三角形为例，来说说片元着色器的绘制结合图形的方法。

首先，我们要判断点是否在三角形内部。我们知道，点到三角形三条边的距离有三个，只要这三个距离的符号都相同，我们就能确定点在三角形内。

然后，我们建立三角形的距离模型。我们规定它的内部距离为负，外部距离为正，并且都选点到三条边的最小距离。代码如下：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

float line_distance(in vec2 st, in vec2 a, in vec2 b) {
  vec3 ab = vec3(b - a, 0);
  vec3 p = vec3(st - a, 0);
  float l = length(ab);
  return cross(p, normalize(ab)).z;
}

float seg_distance(in vec2 st, in vec2 a, in vec2 b) {
  vec3 ab = vec3(b - a, 0);
  vec3 p = vec3(st - a, 0);
  float l = length(ab);
  float d = abs(cross(p, normalize(ab)).z);
  float proj = dot(p, ab) / l;
  if(proj &gt;= 0.0 &amp;&amp; proj &lt;= l) return d;
  return min(distance(st, a), distance(st, b));
}

float triangle_distance(in vec2 st, in vec2 a, in vec2 b, in vec2 c) {
  float d1 = line_distance(st, a, b);
  float d2 = line_distance(st, b, c);
  float d3 = line_distance(st, c, a);

  if(d1 &gt;= 0.0 &amp;&amp; d2 &gt;= 0.0 &amp;&amp; d3 &gt;= 0.0 || d1 &lt;= 0.0 &amp;&amp; d2 &lt;= 0.0 &amp;&amp; d3 &lt;= 0.0) {
    return -min(abs(d1), min(abs(d2), abs(d3))); // 内部距离为负
  }
  
  return min(seg_distance(st, a, b), min(seg_distance(st, b, c), seg_distance(st, c, a))); // 外部为正
}

void main() {
  float d = triangle_distance(vUv, vec2(0.3), vec2(0.5, 0.7), vec2(0.7, 0.3));
  gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

这样，我们就绘制出了一个白色的三角形。

<img src="https://static001.geekbang.org/resource/image/34/9b/34ae3dfdca357d9207ea42e02d1f269b.jpeg" alt="">

实际上，三角形的这种画法还可以推广到任意凸多边形。比如，矩形和正多边形就可以使用同样的方式来绘制。

## 片元着色器绘图方法论：符号距离场渲染

现在，你应该知道这些基本的线段、圆和几何图形该怎么绘制了。那我们能不能从中总结出一套统一的方法论呢？我们发现，前面绘制的图形虽然各不相同，但是它们的绘制步骤都可以总结为以下两步。

**第一步：定义距离**。这里的距离，是一个人为定义的概念。在画圆的时候，它指的是点到圆心的距离；在画直线和线段的时候，它是指点到直线或某条线段的距离；在画几何图形的时候，它是指点到几何图形边的距离。

**第二步：根据距离着色**。首先是用smoothstep方法，选择某个范围的距离值，比如在画直线的时候，我们设置smoothstep(0.0, 0.01, d)，就表示选取距离为0.0到0.01的值。然后对这个范围着色，我们就可以将图形的边界绘制出来了。

延续这个思路，我们还可以选择距离在0.0~0.01范围以外的点。下面，我们做一个小实验。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  vec3 line = vec3(1, 1, 0);
  float d = abs(cross(vec3(vUv,0), normalize(line)).z);
  gl_FragColor.rgb = (smoothstep(0.195, 0.2, d) - smoothstep(0.2, 0.205, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

我们用之前绘制直线的代码，将 gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0) 修改为 gl_FragColor.rgb = (smoothstep(0.195, 0.2, d) - smoothstep(0.2, 0.205, d)) * vec3(1.0)，我们看到输出的结果变成对称的两条直线了。

<img src="https://static001.geekbang.org/resource/image/dc/c9/dcfb68cb31eae8c8f99dbc663a207dc9.jpeg" alt="">

这是为什么呢？因为我们是对距离原直线0.2处的点进行的着色，那实际上距离0.2的点有两条线，所以就能绘制出两条直线了。我把它的原理画了一个示意图，你可以看看，其中红线是原直线。

<img src="https://static001.geekbang.org/resource/image/ba/e5/ba296d4490d0a0c699f6ecc739e003e5.jpeg" alt="">

利用这个思路，再加上使用乘法和fract函数重复绘制的原理，我们就可以绘制多条平行线了。比如通过下面的代码，我们可以绘制出均匀的平面分割线。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  vec3 line = vec3(1, 1, 0);
  float d = abs(cross(vec3(vUv,0), normalize(line)).z);
  d = fract(20.0 * d);
  gl_FragColor.rgb = (smoothstep(0.45, 0.5, d) - smoothstep(0.5, 0.55, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

<img src="https://static001.geekbang.org/resource/image/2a/28/2ac38ebaf3699b4d1edebf4eecc19b28.jpeg" alt="">

利用同样的办法，我们还可以绘制圆环或者三角环或者其他图形的环。因为原理相同，下面我就直接给你展示代码和效果了。

首先是绘制圆环的代码和效果：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

void main() {
  float d = distance(vUv, vec2(0.5));
  d = fract(20.0 * d);
  gl_FragColor.rgb = (smoothstep(0.45, 0.5, d) - smoothstep(0.5, 0.55, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

<img src="https://static001.geekbang.org/resource/image/04/49/04b066db1e7b9ea0003df73e64fcbf49.jpeg" alt="">

然后是绘制三角环的代码和效果：

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

float line_distance(in vec2 st, in vec2 a, in vec2 b) {
  vec3 ab = vec3(b - a, 0);
  vec3 p = vec3(st - a, 0);
  float l = length(ab);
  return cross(p, normalize(ab)).z;
}

float seg_distance(in vec2 st, in vec2 a, in vec2 b) {
  vec3 ab = vec3(b - a, 0);
  vec3 p = vec3(st - a, 0);
  float l = length(ab);
  float d = abs(cross(p, normalize(ab)).z);
  float proj = dot(p, ab) / l;
  if(proj &gt;= 0.0 &amp;&amp; proj &lt;= l) return d;
  return min(distance(st, a), distance(st, b));
}

float triangle_distance(in vec2 st, in vec2 a, in vec2 b, in vec2 c) {
  float d1 = line_distance(st, a, b);
  float d2 = line_distance(st, b, c);
  float d3 = line_distance(st, c, a);

  if(d1 &gt;= 0.0 &amp;&amp; d2 &gt;= 0.0 &amp;&amp; d3 &gt;= 0.0 || d1 &lt;= 0.0 &amp;&amp; d2 &lt;= 0.0 &amp;&amp; d3 &lt;= 0.0) {
    return -min(abs(d1), min(abs(d2), abs(d3))); // 内部距离为负
  }
  
  return min(seg_distance(st, a, b), min(seg_distance(st, b, c), seg_distance(st, c, a))); // 外部为正
}

void main() {
  float d = triangle_distance(vUv, vec2(0.3), vec2(0.5, 0.7), vec2(0.7, 0.3));
  d = fract(20.0 * abs(d));
  gl_FragColor.rgb = (smoothstep(0.45, 0.5, d) - smoothstep(0.5, 0.55, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

<img src="https://static001.geekbang.org/resource/image/c6/0f/c682ed12d9f148261d8ba076209eb90f.jpeg" alt="">

实际上，上面这种绘制图形和环的方式，在图形渲染中有一个专有的名称叫做**符号距离场渲染**（Signed Distance Fields Rendering）。它本质上就是利用空间中的距离分布来着色的。我们把上面的三角环代码换一种渲染方式，你就能看得更清楚一些了。代码如下：

```
void main() {
  float d = triangle_distance(vUv, vec2(0.3), vec2(0.5, 0.7), vec2(0.7, 0.3));
  d = fract(20.0 * abs(d));
  gl_FragColor.rgb = vec3(d);
  // gl_FragColor.rgb = (smoothstep(0.45, 0.5, d) - smoothstep(0.5, 0.55, d)) * vec3(1.0);
  gl_FragColor.a = 1.0;
}

```

我们在渲染的时候，还可以把main函数中原来的smoothstep渲染方式注释掉，直接用vec3(d)来渲染颜色，就会得到的如下的效果。

<img src="https://static001.geekbang.org/resource/image/3e/84/3eaf4531819d34a61e9ea5aacef4cd84.jpeg" alt="">

你能看到，这里的每一环，两两之间的距离是沿着法线方向从0到1的，所以颜色从黑色过渡到白色，这就是三角环的距离场分布。相同颜色值的环线就是距离场的等距线，我们用step或smoothstep的方式将某些等距线的颜色设置为白色，其他位置颜色设置为黑色，就绘制出之前的环线效果来了。

## 着色器绘制几何图形的用途

讨论到这里，你一定有些疑惑，我们学习这些片元着色器的绘图方式，究竟有什么实际用途呢？实际上它的用途还是挺广泛的，在这里我想先简单举几个实际的应用案例，你可以先感受一下。

不过，在讲具体案例之前，我还想多啰嗦几句。着色器造型是着色器的一种非常基础的使用方法，甚至可以说是图形学中着色器渲染最基础的原理，就好比代数的基础是四则运算一样，它构成了GPU视觉渲染的基石，我们在视觉呈现中生成的各种细节特效的方法，万变不离其宗，基本上都和着色器造型有关。

所以呢，我希望你不仅仅要了解它的用途，更要彻底弄明白它的原理和思路，尤其是非常重要的符号距离场渲染技巧，一定要理解并熟练掌握。关于着色器造型的更多、更复杂的应用场景，我们在后续的课程中还会遇到。明白了这一点，我们接着来看三个简单的案例吧。

首先，我们可以用着色器造型实现图像的剪裁。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

uniform sampler2D tMap;
uniform float uTime;

...

void main() {
  vec4 color = texture2D(tMap, vUv);
  vec2 uv = vUv - vec2(0.5);
  vec2 a = vec2(-0.577, 0) - vec2(0.5);
  vec2 b = vec2(0.5, 1.866) - vec2(0.5);
  vec2 c = vec2(1.577, 0) - vec2(0.5);

  float scale = min(1.0, 0.0005 * uTime);
    float d = triangle_distance(uv, scale * a, scale * b, scale * c);
    gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * color.rgb;
    gl_FragColor.a = 1.0;
  }

```

利用上面的代码，我们对图像进行三角形剪裁，可以实现的效果如下：

<img src="https://static001.geekbang.org/resource/image/87/54/877b37250d0e2da82b3e72fcd83b3754.gif" alt="">

其次，我们可以实现对图像的动态修饰，比如类似下面这种进度条。

```
#ifdef GL_ES
precision highp float;
#endif

varying vec2 vUv;

uniform sampler2D tMap;
uniform float uTime;

...

void main() {
  vec4 color = texture2D(tMap, vUv);
  vec2 uv = vUv - vec2(0.5);
  vec2 a = vec2(0, 1);
  float time = 0.0005 * uTime;

  vec2 b = vec2(sin(time), cos(time));
  float d = 0.0;

  float c0 = cross(vec3(b, 0.0), vec3(a, 0.0)).z;
  float c1 = cross(vec3(uv, 0.0), vec3(a, 0.0)).z;
  float c2 = cross(vec3(uv, 0.0), vec3(b, 0.0)).z;
  if(c0 &gt; 0.0 &amp;&amp; c1 &gt; 0.0 &amp;&amp; c2 &lt; 0.0) {
    d = 1.0;
  }
  if(c0 &lt; 0.0 &amp;&amp; (c1 &gt;= 0.0 || c2 &lt;= 0.0)) {
    d = 1.0;
  }

  gl_FragColor.rgb = color.rgb;
  gl_FragColor.r *= mix(0.3, 1.0, d);
  gl_FragColor.a = mix(0.9, 1.0, d);
}


```

<img src="https://static001.geekbang.org/resource/image/f0/54/f07b6c187786c59029ab626c5b605254.gif" alt="">

第三，我们还可以在一些3D场景中修饰几何体，比如像这样给一个球体套一个外壳，这个例子的代码我就不贴出来了，在后续3D课程中我们再详细来说。

<img src="https://static001.geekbang.org/resource/image/4f/09/4f65c00dd8fa1a58bf7128a987d31109.gif" alt="">

## 要点总结

这一节课，我们学习了使用片元着色器进行几何造型的2种常用方法。

首先，用片元着色器可以通过控制局部颜色来绘制图案，比如根据像素坐标来控制颜色变化，然后利用重复绘制的技巧，形成有趣的图案花纹。

其次，我们可以定义并计算像素坐标的距离，然后根据距离来填充颜色，这种方法实际上叫做符号距离场渲染，是着色器造型生成图案的基础方法。通过这种方法我们可以绘制圆、直线、线段、三角形以及其他图形。

使用着色器绘制几何图形是WebGL常用的方式，它有许多用途，比如可以剪裁图像、显示进度条、实现外壳纹路等等，因此在可视化中有许多使用场景。

## 小试牛刀

这一节课我们介绍了圆、直线、线段和三角形的基本画法，其他图形也可以用t方法来绘制。试着用同样的思路来绘制正方形、正六角星、椭圆吧！

欢迎在留言区和我讨论，分享你的答案和思考，也欢迎你把这节课分享给你的朋友，我们下节课再见！

## 源码

[本节课完整代码.](https://github.com/akira-cn/graphics/tree/master/shaping-functions)
