You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用片段着色器绘制直线或贝塞尔曲线?

Nice question! 用片段着色器绘制贝塞尔曲线(或者直线)和你之前在CPU端生成顶点再绘制的思路完全不一样——它是直接在像素层面做判断,不用预先生成一堆插值点。我来一步步给你拆解怎么实现:

核心思路:距离场(Distance Field)

片段着色器绘制曲线的核心逻辑是:对每个像素,计算它到目标曲线的距离,当这个距离小于线宽的一半时,就给这个像素染上曲线的颜色;否则保留背景色。这种方式不需要复杂的顶点数据,只需要一个覆盖全屏的矩形顶点,用来让片段着色器遍历所有像素即可。

步骤1:给着色器传递必要的参数

首先你需要把曲线的关键参数通过uniform传到片段着色器里——因为每个像素的计算都依赖这些数据:

// 片段着色器的uniform变量
uniform vec2 p0, p1, p2, p3; // 三次贝塞尔的4个控制点
uniform float lineWidth;     // 曲线的线宽
uniform vec3 curveColor;     // 曲线颜色
uniform vec3 bgColor;        // 背景颜色

在CPU端,你只需要在绘制前更新这些uniform的值就行,不用再生成和上传顶点数组。

步骤2:实现贝塞尔曲线的距离计算

接下来要在片段着色器里写两个关键函数:一个是计算贝塞尔曲线上某点的参数方程,另一个是计算当前像素到曲线的最小距离。

三次贝塞尔的参数方程

// 根据参数t(范围[0,1])计算贝塞尔曲线上的点
vec2 bezier(float t) {
    float invT = 1.0 - t;
    float invT2 = invT * invT;
    float invT3 = invT2 * invT;
    float t2 = t * t;
    float t3 = t2 * t;
    
    return invT3 * p0 + 3.0 * invT2 * t * p1 + 3.0 * invT * t2 * p2 + t3 * p3;
}

计算像素到曲线的最小距离

最简单的实现是用迭代法,在t的范围内采样多个点,取距离最小的那个值:

// 计算当前像素到贝塞尔曲线的最小距离
float distanceToBezier(vec2 pixelPos) {
    float minDist = 1e6; // 初始化一个很大的距离
    // 采样20个点,次数越多精度越高,20次足够日常使用
    for (int i = 0; i < 20; i++) {
        float t = float(i) / 19.0;
        vec2 curvePoint = bezier(t);
        minDist = min(minDist, length(pixelPos - curvePoint));
    }
    return minDist;
}

最终的片段着色器主函数

void main() {
    vec2 pixelPos = gl_FragCoord.xy; // 获取当前像素的屏幕坐标
    float dist = distanceToBezier(pixelPos);
    
    // 判断是否在曲线范围内,绘制对应颜色
    float halfLineWidth = lineWidth * 0.5;
    if (dist < halfLineWidth) {
        gl_FragColor = vec4(curveColor, 1.0);
    } else {
        gl_FragColor = vec4(bgColor, 1.0);
    }
}
步骤3:直线的特殊处理

直线其实是贝塞尔曲线的特例(当p0=p1且p2=p3时就是直线),但直接写直线的距离计算会更高效:

// 计算像素到直线段的距离
float distanceToLine(vec2 pixelPos, vec2 lineStart, vec2 lineEnd) {
    vec2 lineDir = lineEnd - lineStart;
    float lineLength = length(lineDir);
    lineDir /= lineLength; // 归一化方向向量
    
    vec2 pixelToStart = pixelPos - lineStart;
    float proj = dot(pixelToStart, lineDir); // 投影长度
    
    // 如果投影在直线段外,取到端点的距离
    if (proj < 0.0) return length(pixelToStart);
    if (proj > lineLength) return length(pixelPos - lineEnd);
    
    // 投影在直线段上,取垂直距离
    vec2 projPoint = lineStart + proj * lineDir;
    return length(pixelPos - projPoint);
}

在主函数里调用这个函数就行,不用走贝塞尔的逻辑。

优化小技巧
  1. 抗锯齿处理:不要用硬的if判断,用smoothstep实现平滑的边缘过渡,避免锯齿:
    float halfLineWidth = lineWidth * 0.5;
    // 用smoothstep做平滑过渡,1.0是抗锯齿的模糊宽度
    float alpha = smoothstep(halfLineWidth, halfLineWidth - 1.0, dist);
    gl_FragColor = vec4(mix(bgColor, curveColor, alpha), 1.0);
    
  2. 性能优化:迭代法虽然简单,但如果需要更高性能,可以用贝塞尔曲线的解析距离场公式(推导比较复杂,但有现成的GLSL实现可以直接用),或者用二分法找最近点,减少采样次数。
  3. 多条曲线:如果要绘制多条曲线,可以把控制点打包成数组uniform,比如uniform vec2 bezierCurves[4 * numCurves];,然后在着色器里循环处理每条曲线的距离。
和传统顶点绘制的对比
  • 优点:不需要预先生成大量顶点数据,尤其适合动态调整曲线(比如用户拖拽控制点)的场景,只需要更新uniform,不用重新上传顶点缓冲区。
  • 缺点:计算量和屏幕分辨率成正比,高分辨率下性能可能略低于顶点绘制,但现代GPU完全能应付大多数场景。

内容的提问来源于stack exchange,提问作者JDBones

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 04:33:13