在开发 UI 的时候,难免会遇到设计同学的奇思妙想超出了你的想象范围的情况。比如说,设计同学可能会让你画一个像下面这样不停运动的五角星动画:
这样的线型动画也不只是为了好玩或者好看,在许多实用的领域比如监控图表或者 AI 可视化的过程中也可以发挥各种作用。
使用程序绘制这样的图形,通常会选择直接调用原生的绘制图像 API,比如 OpenCV 的 Drawing Functions,或者 Windows 下的 GDI/GDI+,或者 Web 下面的 Canvas 的绘制接口,或者 Android/iOS 相应的接口等等。
但对于前面图中这样的需求来说,需要在每一帧中计算需要绘制的线是哪些,从哪里开始到哪里结束,根据直线和曲线选用不同的 API,当线有一定宽度的时候,根据采用的 API 不同往往还会遇到转角的地方效果不尽如人意之类的情况,要实现图中的渐隐效果也会很困难。
写定代码之后,要调整曲线形状也很不容易(比如在五角星的尖上面增加一个小圆角),很可能需要推翻重写。实际运行起来时,还可能会发现因为计算过程过于复杂而产生性能问题。更不用说对于 Windows/Qt/Web/Android/iOS 等多个平台,代码是完全不通用的,极大增加了开发成本。
另一个方案是直接使用更为底层的绘图 API 来实现这样的效果,考虑到跨平台的需要,目前 OpenGL 仍然是一个比较稳妥的方案,尤其是许多设备上的浏览器只支持 WebGL 1.0,如果能在 WebGL 1.0(大约相当于 OpenGL ES 2.0)上仅使用 WebGL API 实现这种线型动画,那么绝大多数平台都可以使用相同的方案进行绘制,代码逻辑很容易迁移。本文介绍的就是这样一种方案。
OpenGL 基础知识
OpenGL 接口的不同版本有一些很不同的特性,大体上按照固定管线、可编程管线、进一步扩展的可编程管线进行演进,这里按照 OpenGL ES 2.0 版本进行介绍,涉及到的特性基本上可以在目前主流的所有 OpenGL 平台(包括WebGL)上得到支持。
在 OpenGL ES 2.0 版本的可编程管线流程大致可以描述为:
顶点(vertex)数据 → 顶点着色器(vertex shader) → 顶点后处理 → 光栅化(rasterization) → 片段着色器(fragment shader) → 采样后处理
其中,可编程的部分主要指顶点着色器(vertex shader)和片段着色器(fragment shader)两部分,其他部分的功能相对固定。两种着色器使用被称为 GLSL 的编程语言编写,风格和 C 语言类似,主要控制输入数据经过怎样的运算之后得到输出数据,例如,一个简单的 OpenGL ES 2.0(WebGL 1.0)可用的顶点着色器可以如下编写:
precision highp float;
precision highp int;
attribute vec2 a_position;
uniform mat4 u_transform;
varying vec2 v_position;
void main(){
gl_Position = u_transform * vec4(a_position, 0.0, 1.0);
v_position = a_position;
}
它有一个二维向量类型的顶点属性(attribute)输入 a_position,同时有一个常量(uniform)输入 u_transform,类型是一个 4x4 的矩阵。它输出了 OpenGL 要求的顶点坐标 gl_Position,同时定义了一个输出给片段着色器的结果 v_position。向量被补充为四维齐次坐标向量之后,使用矩阵进行变换,得到的结果作为顶点着色器的输出(用 gl_开头的特定变量表示),同时原始的 a_position 被直接传递给了 v_position,方便在片段着色器里进一步处理。
着色器会针对每个要处理的单元运行一次,顶点着色器是对每个顶点执行一次,片段着色器则是对每个