如何使用片段着色器绘制直线或贝塞尔曲线?
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); }
在主函数里调用这个函数就行,不用走贝塞尔的逻辑。
优化小技巧
- 抗锯齿处理:不要用硬的
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); - 性能优化:迭代法虽然简单,但如果需要更高性能,可以用贝塞尔曲线的解析距离场公式(推导比较复杂,但有现成的GLSL实现可以直接用),或者用二分法找最近点,减少采样次数。
- 多条曲线:如果要绘制多条曲线,可以把控制点打包成数组uniform,比如
uniform vec2 bezierCurves[4 * numCurves];,然后在着色器里循环处理每条曲线的距离。
和传统顶点绘制的对比
- 优点:不需要预先生成大量顶点数据,尤其适合动态调整曲线(比如用户拖拽控制点)的场景,只需要更新uniform,不用重新上传顶点缓冲区。
- 缺点:计算量和屏幕分辨率成正比,高分辨率下性能可能略低于顶点绘制,但现代GPU完全能应付大多数场景。
内容的提问来源于stack exchange,提问作者JDBones
相关产品推荐
相关产品推荐

