使用 QPainter 绘制带有渐变边缘的直线和曲线

Posted

技术标签:

【中文标题】使用 QPainter 绘制带有渐变边缘的直线和曲线【英文标题】:Draw a line and curves with fading edges with QPainter 【发布时间】:2020-01-04 14:53:03 【问题描述】:

QPainter 非常易于使用,画一条线只需这样做:

QPainter painter(&image);
QPen pen;
pen.setWidth(5);
pen.setColor("black");
painter.setPen(pen);
painter.drawLine(QPointF(0,0), QPointF(200,250));

现在这很好用,但我想创建一个“特殊”的笔,产生带有“平滑”边缘的线条。例如,假设我要绘制的线条的粗细为 10 像素,那么我希望线条的中间(我指的是粗细而不是长度)完全不透明,并且线条的边缘应该变成半透明。我相信这可以得到,例如将下面的图片作为我的点,然后“拖动”并绘制线条,这样我将获得我想要的效果。我知道 Qt 为您提供了QBrush 和渐变,但我不知道该怎么做。

【问题讨论】:

qt 是否允许您进行逐像素操作?您可以将单个像素的值设置为任意颜色吗? @Makogan 是的,可以通过QPainter::drawPoint 做到这一点 好的,这根本不是解决此问题的有效方法,但根据您的要求,它可能已经足够好了。让你的 10 像素线让其中 4 个像素完全不透明,所以你只需画一条粗细为 4 的常规线。然后画一条粗细为 1 的线,平行于前一条线,但沿正交偏移一个像素该线的方向(旋转 90 度的方向矢量)您不断绘制这些平行线,当您远离原始线时,它们会逐渐透明。 如果您可以通过 QT 访问着色器,我认为它支持,您可以创建自己的自定义线着色器,通过绘制一个矩形而不是默认的 QT 线来为您执行此操作。 这就是抗锯齿的工作原理。着色器上正确的线抗锯齿算法是。您绘制一个矩形,然后相对于当前渲染点到线的距离从不透明插入到透明度。即您有一个矩形,该矩形的中线是您要渲染的线。该矩形上点的透明度与到线的距离成正比。我不确定的是 QT 给你多少控制来操纵它的着色管道。 【参考方案1】:

虽然 Qt 肯定有 many drawing functions,但没有一个可以让您使用像素图画笔沿着您描述的路径绘制。

我可以想到 2 种方法来实现您想要的:

    使用标准非模糊画笔多次绘制路径,画笔宽度不同,颜色透明。通过足够的迭代,这将接近您正在寻找的“模糊线”。

    沿路径重复绘制像素图。这通常是像 photoshop 或 gimp 这样的绘图软件会这样做的,因为它允许参数具有一定的灵活性,例如不同的画笔像素图和方向等。

我将尝试提供示例代码(目前尚未测试,因为我目前不在我的开发计算机旁)在这里进行第二种方式:

void drawPathWithPixmapBrush(QPainter painter, QPainterPath path, QPixmap pixmapBrush, qreal spacing=1.0) 
    qreal length = path.length();
    qreal pos = 0.0;
    // Adjust the spacing to be relative to brush size
    spacing=(spacing * pixmapBrush.width() );
    while (pos < length) 
        qreal percent = path.percentAtLength(pos);
painter.drawPixmap(path.pointAtPercent(percent), pixmapBrush);
pos += spacing;
    

如您所见,此代码将沿着给定的QPainterPath 迭代移动,并且对于每一步将QPixmap 画笔绘制到给定的QPainter,从而可以预测为沿着带有像素图画笔的路径。

QPainterPath 支持 QPainter 所做的所有绘图操作,例如多边形、样条线、直线、拱形等,因此它将在很大程度上替代您现有的 QPainter 绘图调用(请参阅the full list of drawing operations)。

这是使用QPainterPath 构造简单直线段的示例代码:

QPainterPath path
QPointF lastPosition(10, 10);
QPointF currentPosition(100, 100);
path.moveTo(lastPosition);
path.lineTo(currentPosition);

可能的改进包括计算间距以使其恰好在行尾结束,并且可能更正确地计算所需的绘制次数将最好地给人以行的印象。

【讨论】:

我尝试过类似的方法。但是,我遇到了两个问题:1.当曲线很长时,您绘制了很多点并且性能不是最佳的(在我的实时绘图应用程序中有很多延迟)2.很难获得间距是对的,我尝试使用 qreal spacing = brushSize/path.length(),但我得到的路径适用于特定长度的路径。 @daljit97:不幸的是,这就是野兽的现实。使用第一种方法,您可能会获得更好的性能/质量。我会看看我是否可以为它写一个伪代码。另一种选择是放弃 Qt 绘图原语及其带来的加速/性能,而只实现您自己的渲染器。但那完全是另一回事了。 是的,自定义渲染将是矫枉过正。如果我能很好地渲染曲线,我会很高兴。尽管性能欠佳,但我可以通过让用户在“实时”模式下绘制法线然后重新绘制具有所需效果的曲线来解决。真正的问题是我的 QPainterPath 由几条不同长度的线组成,我找不到一个可以为所有长度提供漂亮平滑线的间距方程(如果我使用的间距太小,由于重叠和如果它太大,那么“球”就会被隔开并断开)。 @daljit97 :如果您考虑一下,问题就变成了选择要绘制多少个点的问题。这将使您有机会线性地用性能换取质量,因为更多的点更慢更好,更少的点更快更丑。一种方法是根据最后一帧的渲染时间自适应地选择点数。您需要弄清楚的另一件事是绘制像素图时使用的透明度。您可以使用两个相邻圆圈的重叠区域 (milania.de/blog/…) 我不确定改变像素图的透明度是否能解决这个问题。无论如何,我已经创建了一个小的 repository 并使用了 QRadialGradient,正如另一个答案中所建议的那样。效果接近我想要的,但曲线并不完美。【参考方案2】:

实现此目的的一种方法是使用QRadialGradient 作为 QPen 的画笔:

QPointF centerPoint(400, 400);
qreal centerRadius = 200;

QRadialGradient radialGrad(centerPoint, centerRadius);
radialGrad.setColorAt(0.000, QColor(0, 0, 0, 255));
radialGrad.setColorAt(0.8, QColor(0, 0, 0, 0.8 * 255));
radialGrad.setColorAt(1.000, QColor(0, 0, 0, 0.000));

QPen pen;
pen.setWidth(400);
pen.setColor("black");
pen.setBrush(radialGrad);

QPainter painter(this);
painter.setPen(pen);
painter.drawPoint(centerPoint);

这种技术的缺点是渐变在 GradientStop 附近不平滑。您应该添加几个 GradientStop 来缓和径向渐变。

实现此目的的另一种(更漂亮的)方法是创建一个自定义 QBrush,其具有代表所需画笔图案的专用纹理或纹理图像(使用方法 QBrush::setTexture 或 QBrush::setTextureImage)。

【讨论】:

但这可以用于绘制曲线吗?我尝试使用 bresenham 算法,但曲线不连续 是的。使用这两种技术,您可以将这些点沿着一条线/曲线展开,它们之间有右边距(取决于纹理的大小或点半径)。不知道直接使用painter.drawLine(..)方法的解决方案(如果有的话)。 我已经尝试实现这个here,但是我仍然找不到使曲线平滑的方法。【参考方案3】:

作为一个试探性的答案。我知道你说 QT,但我不熟悉它。我会给你一些我在自己的工作中使用的 GLSL 代码,希望对你有所帮助。

在最简单的实现中,可以通过根据与所需线的距离选择矩形中某个点的 alpha 值来对线抗锯齿进行编码。也就是说,您渲染一个矩形,对于该矩形中的每个点,您决定它与线的距离,并根据该距离选择其 alpha 值。例如在片段着色器中可以这样写:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec2 p1;
layout(location = 1) in vec2 p2;
layout(location = 2) in vec2 frag_pos;

layout(location = 0) out vec4 outColor;

layout(binding = 0) uniform LineProperties 
    vec4 offset;
    vec4 color;
    float aspect_ratio;
    float line_width;
;

void main() 

    vec2 line_dir = normalize(p2 - p1);
    vec2 proj = dot(frag_pos - p1, line_dir) * line_dir;
    float distance = length((frag_pos - p1) - proj);
    float coeff = distance / line_width;
    float intensity = 1 - smoothstep(0.2, 0.9, coeff);
    outColor = vec4(intensity * color.xyz, intensity);

这可以给你这样的行:

如果您更仔细地检查该图像,您会注意到直线段之间的接缝处存在一些间隙。这可以通过不计算到直线的最短距离来避免,而是计算到小于直线的段的最短距离,这将使您的直线区域看起来有点像 2D 药丸/胶囊,also known as a stadium。

两个相邻体育场的中心应该相等,这样所有的差距都会消失。

【讨论】:

以上是关于使用 QPainter 绘制带有渐变边缘的直线和曲线的主要内容,如果未能解决你的问题,请参考以下文章

利用QPainter绘制各种图形(Shape, Pen 宽带,颜色,风格,Cap,Join,刷子)

如何在 iOS 中绘制带有弯曲边缘的弧?

如何在 SKShapeNode 中绘制渐变?

绘图QPainter-画刷

UIBezierPath下绘制渐变

Qt之渐变及其应用(绘制温度计仪表盘和指示灯)