webGL中圆角矩形的绘制

前言

在写HTML这类自带样式设置的渲染语言时,可能写出一个圆角矩形仅仅就是一个样式设置就搞定了;但是有没有想过,直接在webGL中用原生的着色器算法写出一个类似CSS圆角样式的效果怎么做?

四角等价圆角效果的实现

所谓的四角等价圆角效果就是指:暂时考虑四个角的圆角效果一致,且圆角在水平和竖直方向的半径是一致的,换言之就是四个角共用一个参数;因为像CSS圆角效果目前是可以分别控制四个角以及圆角水平及竖直方向的半径的。

绘制算法简单的来说就是:

  1. 找到矩形区域的像素;
  2. 抠掉圆角区域外的像素;
  3. 圆角边缘区域抗锯齿处理(光滑过渡);

矩形区域

无论外层抽象数据的矩形区域如何确认,用何种距离单位;输入到顶点着色器内时,都需要将顶点坐标转为NDC坐标(标准化设备坐标),这之间的转换无非就是一个二维坐标系之间的映射

img

得到了矩形区域的顶点坐标,输入到片元着色器内就可以得到插值后的片元坐标(可以看做是像素坐标),然后进一步根据片元坐标来判断相应片元是否处于圆角区域之外了;

抠掉圆角区域外的像素

有了插值化的顶点NDC坐标和圆角半径参数就可以对圆角外的区域进行计算了,考虑到实际上是以像素作为单位,因此可以构建一个以矩形左上角为原点,单位为像素的本地化坐标系,这样便于进行计算;

  1. 分别计算四个角对应的圆中心位置在矩形本地坐标系中的位置;
  2. 判断片元是否位于四个角的区域内;
  3. 若片元位于四个角的区域内,判断是否位于中心圆的区域之外,位于区域外即可舍去(抠掉);

img

上述做法听起来很不错,但是这种像素级别的图形光栅化都会有一个问题,那就是曲线边缘会出现锯齿

img

抗锯齿处理

通用的抗锯齿做法就是在边缘附近做一个光滑过渡,而非粗暴的直接在边缘处进行直接切割;

  1. 以边缘处(阈值,此处为圆角半径长度)为中心,将±1像素长度的范围内的值进行插值;
  2. 插值函数可以直接用GLSL自带的smoothstep方法即可;

上述方法得到的效果如下:

img

可以看到圆角边缘处光滑多了;

着色器代码

下面是针对上述算法实现的一个片元着色器,仅供参考;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
precision highp float; // 高精度
uniform vec2 u_Screen; // 屏幕尺寸
uniform vec4 u_Background; // 背景色
uniform float u_Radius; // 圆角半径
uniform vec2 u_Size; // 矩形尺寸(px)
varying vec2 v_Local; // 局部坐标
uniform bool u_LinearGradient; // 是否为线性渐变填充
uniform vec4 u_FromColor;
uniform vec4 u_ToColor;
vec2 topLeft = vec2(1.0) + u_Radius;
vec2 topRight = vec2(u_Size.x - u_Radius, 1.0 + u_Radius);
vec2 bottomLeft = vec2(1.0 + u_Radius, u_Size.y - u_Radius);
vec2 bottomRight = u_Size - u_Radius;

#pragma glslify: linearGradient = require(./shader-function/linearGradient.glsl)

/** 判断是否为圆角外区域 */
float isCorner() {
float d = 0.0; // 距离所属圆角中心的距离
if (v_Local.x < u_Radius + 1.0 && v_Local.y < u_Radius + 1.0) { // 左上
d = distance(v_Local, topLeft);
}
if (v_Local.x > u_Size.x - u_Radius && v_Local.y < u_Radius + 1.0) { // 右上
d = distance(v_Local, topRight);
}
if (v_Local.x < u_Radius + 1.0 && v_Local.y > u_Size.y - u_Radius) { // 左下
d = distance(v_Local, bottomLeft);
}
if (v_Local.x > u_Size.x - u_Radius && v_Local.y > u_Size.y - u_Radius) { // 右下
d = distance(v_Local, bottomRight);
}
return 1.0 - smoothstep(u_Radius - 1.0, u_Radius + 1.0, d); // ±1像素的过渡光滑处理
}

void main() {
float corner = isCorner();
vec4 bgColor = u_LinearGradient ? linearGradient(u_FromColor, u_ToColor, u_Size, v_Local) : u_Background;
if (corner == 0.0) {
discard; // 废弃像素渲染比透明色更好
} else {
gl_FragColor = corner * bgColor;
}
}

相关文档