是否可以仅使用 GPU 来加厚二次 Bézier 曲线?

发布于 2024-10-29 16:26:57 字数 1164 浏览 0 评论 0原文

我在 OpenGL 程序中绘制了大量二次贝塞尔曲线。现在,曲线只有一像素细,并且是由软件生成的,因为我还处于相当早期的阶段,看看什么有效就足够了。

很简单,给定 3 个控制点(P0P2),我用 < em>t 在软件中从 0 变化到 1(步长为 1/8),并使用 GL_LINE_STRIP 将它们链接在一起:

B(t) = (1 - t)2P0 + 2(1 - t)tP1 + t2P2

其中 B 显然会产生一个二维向量。

这种方法效果“足够好”,因为即使是最大的曲线也不需要超过 8 个步骤即可看起来弯曲。尽管如此,一像素的细曲线还是很难看。

我想编写一个 GLSL 着色器,它接受控制点和统一的厚度变量,以使曲线更粗。起初,我考虑只制作一个像素着色器,它只会对曲线厚度/2距离内的像素进行着色,但这样做需要求解三次多项式,并在一个内的三个解决方案之间进行选择着色器看起来并不是最好的主意。

然后我尝试查找其他人是否已经这样做了。我偶然发现了微软研究院 Loop 和 Blinn 撰写的白皮书 他们展示了一种填充曲线下区域的简单方法。虽然它在某种程度上运作良好,但我很难将这个想法适应在两条鲍德曲线之间进行绘制。

使用几何着色器查找与单条曲线匹配的边界曲线相当容易。问题来自于应该填充整个事物的片段着色器。他们的方法使用插值纹理坐标来确定片段是否落在曲线上方或下方;但我想不出用两条曲线来做到这一点的方法(我对着色器很陌生,而且不是数学专家,所以我不知道如何做到这一点并不意味着这是不可能的)。

我的下一个想法是将填充曲线分成三角形,并仅在外部部分使用贝塞尔片段着色器。但为此,我需要在可变点处分割内部曲线和外部曲线,这再次意味着我必须求解方程,这实际上不是一个选择。

是否有使用着色器绘制二次贝塞尔曲线的可行算法?

I draw lots of quadratic Bézier curves in my OpenGL program. Right now, the curves are one-pixel thin and software-generated, because I'm at a rather early stage, and it is enough to see what works.

Simply enough, given 3 control points (P0 to P2), I evaluate the following equation with t varying from 0 to 1 (with steps of 1/8) in software and use GL_LINE_STRIP to link them together:

B(t) = (1 - t)2P0 + 2(1 - t)tP1 + t2P2

Where B, obviously enough, results in a 2-dimensional vector.

This approach worked 'well enough', since even my largest curves don't need much more than 8 steps to look curved. Still, one pixel thin curves are ugly.

I wanted to write a GLSL shader that would accept control points and a uniform thickness variable to, well, make the curves thicker. At first I thought about making a pixel shader only, that would color only pixels within a thickness / 2 distance of the curve, but doing so requires solving a third degree polynomial, and choosing between three solutions inside a shader doesn't look like the best idea ever.

I then tried to look up if other people already did it. I stumbled upon a white paper by Loop and Blinn from Microsoft Research where the guys show an easy way of filling the area under a curve. While it works well to that extent, I'm having trouble adapting the idea to drawing between two bouding curves.

Finding bounding curves that match a single curve is rather easy with a geometry shader. The problems come with the fragment shader that should fill the whole thing. Their approach uses the interpolated texture coordinates to determine if a fragment falls over or under the curve; but I couldn't figure a way to do it with two curves (I'm pretty new to shaders and not a maths expert, so the fact I didn't figure out how to do it certainly doesn't mean it's impossible).

My next idea was to separate the filled curve into triangles and only use the Bézier fragment shader on the outer parts. But for that I need to split the inner and outer curves at variable spots, and that means again that I have to solve the equation, which isn't really an option.

Are there viable algorithms for stroking quadratic Bézier curves with a shader?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

天煞孤星 2024-11-05 16:26:57

这在一定程度上延续了我之前的答案,但实际上有很大不同,因为我在该答案中犯了一些核心问题。

为了允许片段着色器仅在两条曲线之间进行着色,提供了两组“纹理”坐标作为变化变量,并应用了 Loop-Blinn 技术。

varying vec2 texCoord1,texCoord2;
varying float insideOutside;

varying vec4 col;

void main()
{   
    float f1 = texCoord1[0] * texCoord1[0] - texCoord1[1];
    float f2 = texCoord2[0] * texCoord2[0] - texCoord2[1];

    float alpha = (sign(insideOutside*f1) + 1) * (sign(-insideOutside*f2) + 1) * 0.25;
    gl_FragColor = vec4(col.rgb, col.a * alpha);
}

到目前为止,很容易。困难的部分是在几何着色器中设置纹理坐标。 Loop-Blinn 为控制三角形的三个顶点指定它们,并且它们在三角形上进行适当的插值。但是,这里我们需要在实际渲染不同的三角形时具有相同的可用插值。

解决方案是找到从 (x,y) 坐标到内插/外推值的线性函数映射。然后,可以在渲染三角形时为每个顶点设置这些值。这是我这部分代码的关键部分。

    vec2[3] tex = vec2[3]( vec2(0,0), vec2(0.5,0), vec2(1,1) );

    mat3 uvmat;
    uvmat[0] = vec3(pos2[0].x, pos2[1].x, pos2[2].x);
    uvmat[1] = vec3(pos2[0].y, pos2[1].y, pos2[2].y);
    uvmat[2] = vec3(1, 1, 1);

    mat3 uvInv = inverse(transpose(uvmat));

    vec3 uCoeffs = vec3(tex[0][0],tex[1][0],tex[2][0]) * uvInv;
    vec3 vCoeffs = vec3(tex[0][1],tex[1][1],tex[2][1]) * uvInv;

    float[3] uOther, vOther;
    for(i=0; i<3; i++) {
        uOther[i] = dot(uCoeffs,vec3(pos1[i].xy,1));
        vOther[i] = dot(vCoeffs,vec3(pos1[i].xy,1));
    }   

    insideOutside = 1;
    for(i=0; i< gl_VerticesIn; i++){
        gl_Position = gl_ModelViewProjectionMatrix * pos1[i];
        texCoord1 = tex[i];
        texCoord2 = vec2(uOther[i], vOther[i]);
        EmitVertex();
    }
    EndPrimitive();

这里 pos1 和 pos2 包含两个控制三角形的坐标。该部分渲染由 pos1 定义的三角形,但将 texCoord2 设置为 pos2 三角形的转换值。然后 pos2 三角形需要渲染,类似。然后需要填充这两个三角形两端之间的间隙,并适当平移两组坐标。

矩阵逆的计算需要 GLSL 1.50 或者需要手动编码。最好求解平移方程而不计算逆矩阵。不管怎样,我不希望这部分在几何着色器中特别快。

This partly continues my previous answer, but is actually quite different since I got a couple of central things wrong in that answer.

To allow the fragment shader to only shade between two curves, two sets of "texture" coordinates are supplied as varying variables, to which the technique of Loop-Blinn is applied.

varying vec2 texCoord1,texCoord2;
varying float insideOutside;

varying vec4 col;

void main()
{   
    float f1 = texCoord1[0] * texCoord1[0] - texCoord1[1];
    float f2 = texCoord2[0] * texCoord2[0] - texCoord2[1];

    float alpha = (sign(insideOutside*f1) + 1) * (sign(-insideOutside*f2) + 1) * 0.25;
    gl_FragColor = vec4(col.rgb, col.a * alpha);
}

So far, easy. The hard part is setting up the texture coordinates in the geometry shader. Loop-Blinn specifies them for the three vertices of the control triangle, and they are interpolated appropriately across the triangle. But, here we need to have the same interpolated values available while actually rendering a different triangle.

The solution to this is to find the linear function mapping from (x,y) coordinates to the interpolated/extrapolated values. Then, these values can be set for each vertex while rendering a triangle. Here's the key part of my code for this part.

    vec2[3] tex = vec2[3]( vec2(0,0), vec2(0.5,0), vec2(1,1) );

    mat3 uvmat;
    uvmat[0] = vec3(pos2[0].x, pos2[1].x, pos2[2].x);
    uvmat[1] = vec3(pos2[0].y, pos2[1].y, pos2[2].y);
    uvmat[2] = vec3(1, 1, 1);

    mat3 uvInv = inverse(transpose(uvmat));

    vec3 uCoeffs = vec3(tex[0][0],tex[1][0],tex[2][0]) * uvInv;
    vec3 vCoeffs = vec3(tex[0][1],tex[1][1],tex[2][1]) * uvInv;

    float[3] uOther, vOther;
    for(i=0; i<3; i++) {
        uOther[i] = dot(uCoeffs,vec3(pos1[i].xy,1));
        vOther[i] = dot(vCoeffs,vec3(pos1[i].xy,1));
    }   

    insideOutside = 1;
    for(i=0; i< gl_VerticesIn; i++){
        gl_Position = gl_ModelViewProjectionMatrix * pos1[i];
        texCoord1 = tex[i];
        texCoord2 = vec2(uOther[i], vOther[i]);
        EmitVertex();
    }
    EndPrimitive();

Here pos1 and pos2 contain the coordinates of the two control triangles. This part renders the triangle defined by pos1, but with texCoord2 set to the translated values from the pos2 triangle. Then the pos2 triangle needs to be rendered, similarly. Then the gap between these two triangles at each end needs to filled, with both sets of coordinates translated appropriately.

The calculation of the matrix inverse requires either GLSL 1.50 or it needs to be coded manually. It would be better to solve the equation for the translation without calculating the inverse. Either way, I don't expect this part to be particularly fast in the geometry shader.

美胚控场 2024-11-05 16:26:57

您应该能够使用您提到的论文中的 Loop 和 Blinn 技术。

基本上,您需要在法线方向上双向偏移每个控制点,以获得两条曲线(内部和外部)的控制点。然后遵循 Loop 和 Blinn 第 3.1 节中的技术 - 这会分解曲线的各个部分以避免三角形重叠,然后对内部的主要部分进行三角测量(请注意,这部分需要 CPU)。最后,这些三角形被填充,并且它们外面的小弯曲部分使用 Loop 和 Blinn 的技术在 GPU 上渲染(在第 3 节的开头和结尾)。

这里描述了一种可能适合您的替代技术:
OpenGL 中的厚贝塞尔曲线

编辑:
啊,你甚至想避免 CPU 三角测量 - 我应该更仔细地阅读。

您遇到的一个问题是几何着色器和片段着色器之间的接口 - 几何着色器需要生成图元(最有可能是三角形),然后通过片段程序单独光栅化和填充这些图元。

在厚度恒定的情况下,我认为非常简单的三角测量将起作用 - 对所有“弯曲位”使用 Loop 和 Bling。当两个控制三角形不相交时,这很容易。当他们这样做时,十字路口以外的部分就很容易了。所以唯一困难的部分是在交叉点内(应该是一个三角形)。

在交叉点内,只有当两个控制三角形都通过 Loop 和 Bling 导致像素着色时,您才需要对像素进行着色。因此片段着色器需要能够对两个三角形进行纹理查找。一个可以作为标准,您需要为第二组纹理坐标添加一个 vec2 变化变量,您需要为三角形的每个顶点进行适当的设置。此外,您还需要一个统一的“sampler2D”纹理变量,然后您可以通过texture2D 对其进行采样。然后,您只需对满足两个控制三角形(在交叉点内)检查的片段进行着色即可。

我认为这在任何情况下都有效,但我可能错过了一些东西。

You should be able to use technique of Loop and Blinn in the paper you mentioned.

Basically you'll need to offset each control point in the normal direction, both ways, to get the control points for two curves (inner and outer). Then follow the technique in Section 3.1 of Loop and Blinn - this breaks up sections of the curve to avoid triangle overlaps, and then triangulates the main part of the interior (note that this part requires the CPU). Finally, these triangles are filled, and the small curved parts outside of them are rendered on the GPU using Loop and Blinn's technique (at the start and end of Section 3).

An alternative technique that may work for you is described here:
Thick Bezier Curves in OpenGL

EDIT:
Ah, you want to avoid even the CPU triangulation - I should have read more closely.

One issue you have is the interface between the geometry shader and the fragment shader - the geometry shader will need to generate primitives (most likely triangles) that are then individually rasterized and filled via the fragment program.

In your case with constant thickness I think quite a simple triangulation will work - using Loop and Bling for all the "curved bits". When the two control triangles don't intersect it's easy. When they do, the part outside the intersection is easy. So the only hard part is within the intersection (which should be a triangle).

Within the intersection you want to shade a pixel only if both control triangles lead to it being shaded via Loop and Bling. So the fragment shader needs to be able to do texture lookups for both triangles. One can be as standard, and you'll need to add a vec2 varying variable for the second set of texture coordinates, which you'll need to set appropriately for each vertex of the triangle. As well you'll need a uniform "sampler2D" variable for the texture which you can then sample via texture2D. Then you just shade fragments that satisfy the checks for both control triangles (within the intersection).

I think this works in every case, but it's possible I've missed something.

橘虞初梦 2024-11-05 16:26:57

我不知道如何具体解决这个问题,但这很有趣。我认为你需要 GPU 中的每个不同的处理单元:

顶点着色器
将正常的点线扔到顶点着色器。让顶点着色器将点替换为贝塞尔曲线。

几何着色器

让几何着色器为每个顶点创建一个额外的点。

foreach (point p in bezierCurve)
    new point(p+(0,thickness,0)) // in tangent with p1-p2        

片段着色器
要使用特殊描边来描画贝塞尔曲线,您可以使用带有 Alpha 通道的纹理。您可以检查 Alpha 通道的值。如果为零,则剪裁像素。这样,您仍然可以让系统认为它是一条实线,而不是半透明的线。您可以在 Alpha 通道中应用一些模式。

我希望这对您有所帮助。你必须自己解决很多问题,但我认为几何着色会加速你的贝塞尔曲线。


对于抚摸,我仍然选择创建 GL_QUAD_STRIP 和 alpha 通道纹理。

I don't know how to exactly solve this, but it's very interesting. I think you need every different processing unit in the GPU:

Vertex shader
Throw a normal line of points to your vertex shader. Let the vertex shader displace the points to the bezier.

Geometry shader

Let your geometry shader create an extra point per vertex.

foreach (point p in bezierCurve)
    new point(p+(0,thickness,0)) // in tangent with p1-p2        

Fragment shader
To stroke your bezier with a special stroke, you can use a texture with an alpha channel. You can check the alpha channel on its value. If it's zero, clip the pixel. This way, you can still make the system think it is a solid line, instead of a half-transparent one. You could apply some patterns in your alpha channel.

I hope this will help you on your way. You will have to figure out things yourself a lot, but I think that the Geometry shading will speed your bezier up.


Still for the stroking I keep with my choice of creating a GL_QUAD_STRIP and an alpha-channel texture.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文