WebGL交互式背景:Bayer抖动算法快速指南

本文详细介绍了如何使用Bayer抖动模式在WebGL中创建细腻的交互式背景效果。通过代码示例展示矩阵生成、UV映射和实时交互实现,实现高性能的视觉体验。

WebGL交互式背景:Bayer抖动算法快速指南

用户体验依赖于那些巧妙融入整体设计而不喧宾夺主的小细节。这种平衡很难把握,特别是在使用WebGL等技术时。虽然它们能创造惊人的视觉效果,但如果处理不当,也可能变得过于复杂和分散注意力。

Bayer抖动模式就是一种微妙但有效的技术。例如,JetBrains最近的Junie活动页面就使用这种方法营造了一个沉浸式且引人入胜的氛围,同时保持了视觉平衡和可访问性。

在本教程中,我将介绍Bayer抖动模式,解释它的原理和工作方式,以及如何将其应用到你的网页项目中,以增强视觉深度而不影响用户体验。

Bayer抖动

Bayer模式是一种有序抖动类型,允许你使用固定矩阵模拟渐变和深度。

如果我们适当缩放这个矩阵,就可以定位特定值并创建基本模式。

以下是一个简单示例:

1
2
3
4
5
6
7
8
9
// 2×2 Bayer矩阵模式:返回[0,1)范围内的值
float Bayer2(vec2 a)
{
    a = floor(a);                // 使用整数单元格坐标
    return fract(a.x / 2.0 + a.y * a.y * 0.75);
    // 等效查找表:
    // (0,0) → 0.0,  (1,0) → 0.5
    // (0,1) → 0.75, (1,1) → 0.25
}

让我们看一个使用示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 1. 基础遮罩:左半部分为黑白渐变
float mask = uv.y;

// 2. 右半部分:应用有序抖动
if (uv.x > 0.5) {
    float dither = Bayer2(fragCoord);
    mask += dither - 0.5;
    mask  = step(0.5, mask); // 二进制阈值
}

// 3. 输出结果
fragColor = vec4(vec3(mask), 1.0);

仅用一个小矩阵,我们就获得了四个不同的抖动值——基本上是免费的。

创建背景效果

这仍然相当基础——在用户体验方面还不够令人兴奋。让我们通过在UV地图上创建网格来进一步推进。我们将定义"像素"的大小和确定每个"像素"开关状态的矩阵大小,使用Bayer排序。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const float PIXEL_SIZE = 10.0; // Bayer矩阵中每个像素的大小
const float CELL_PIXEL_SIZE = 5.0 * PIXEL_SIZE; // 5x5矩阵

float aspectRatio = uResolution.x / uResolution.y;

vec2 pixelId = floor(fragCoord / PIXEL_SIZE); 
vec2 cellId = floor(fragCoord / CELL_PIXEL_SIZE); 
vec2 cellCoord = cellId * CELL_PIXEL_SIZE;

vec2 uv = cellCoord/uResolution * vec2(aspectRatio, 1.0);

vec3 baseColor = vec3(uv, 0.0);

你会看到一个渲染的UV网格,蓝色点表示像素,白色(及后续相同大小的块)表示Bayer矩阵。

递归Bayer矩阵

Bayer的天才之处在于递归生成的遮罩,既保持了噪声的高频特性,又降低了代码复杂度。现在让我们尝试一下,并应用更大的抖动矩阵:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
float Bayer2(vec2 a) { a = floor(a); return fract(a.x / 2. + a.y * a.y * .75); }
#define Bayer4(a)   (Bayer2(0.5 * (a)) * 0.25 + Bayer2(a))
#define Bayer8(a)   (Bayer4(0.5 * (a)) * 0.25 + Bayer2(a))
#define Bayer16(a)   (Bayer8(0.5 * (a)) * 0.25 + Bayer2(a))

...
if(uv.x > .2) dither = Bayer2 (pixelId);   
if(uv.x > .4) dither = Bayer4 (pixelId);
if(uv.x > .6) dither = Bayer8 (pixelId);
if(uv.x > .8) dither = Bayer16(pixelId);
...

这给我们提供了一个很好的视觉过渡,从基本的UV网格到复杂度递增的Bayer矩阵(2×2、4×4、8×8、16×16)。

如你所见,8×8和16×16模式非常相似——超过8×8后,感知增益变得很小。因此我们将在下一步中使用Bayer8。

现在,我们将Bayer8应用于由fbm噪声调制的UV地图,使结果感觉更加有机——正如我们承诺的那样。

添加交互性

这是最令人兴奋的部分:实时交互性是背景视频无法复制的。让我们使用抖动模式在点击点周围运行涟漪效果。我们将遍历所有活动点击并计算波:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
for (int i = 0; i < MAX_CLICKS; ++i) {

    // 将此点击转换为方形单位UV
    vec2 pos = uClickPos[i];
    if(pos.x < 0.0 && pos.y < 0.0) continue; // 跳过空点击
        
    vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution) )) * vec2(aspectRatio, 1.0);

    float t = max(uTime - uClickTimes[i], 0.0);
    float r = distance(uv, cuv);

    float waveR = speed * t;
    float ring  = exp(-pow((r - waveR) / thickness, 2.0));
    float atten = exp(-dampT * t) * exp(-dampR * r);

    feed = max(feed, ring * atten);           // 最亮者获胜
}

尝试点击下面的CodePen:

最终思考

由于整个Bayer抖动背景是在单个GPU通道中生成的,即使在4K分辨率下也能在0.2毫秒内渲染完成,大小约为3KB(本例中加上Three.js),并且在加载后消耗零网络带宽。一旦你有数千个节点,SVG就无法做到这一点,而自动播放视频在带宽、CPU和电池消耗上要重两个数量级。简而言之:这可能是当今开放网络上可以构建的最轻量级的完全交互式背景效果之一。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计