梯度“斜接”在 OpenGL 中显示连接处的接缝

发布于 2024-10-09 19:08:14 字数 2058 浏览 0 评论 0原文

我正在围绕 GL 中的一些 2D 工作做一些非常基本的实验。我正在尝试在矩形区域周围绘制一个“相框”。我希望框架始终具有一致的渐变,因此我使用看起来像四个四边形的几何体来构造它,框架的每一侧各有一个,逐渐变细以形成有效地具有斜接连接的梯形。

“内部”和“外部”矩形的垂直坐标相同,所有内部和外部的颜色也相同,所以我希望看到边缘的完美混合。

但请注意下图中,连接角处似乎有一条“接缝”,它比应有的颜色要浅。

我觉得我在数学上遗漏了一些解释这一点的概念。这个伪影是否是梯度斜率的结果?如果我将所有颜色更改为不透明蓝色(例如),我将得到预期的完美纯蓝色框架。

更新:下面添加了代码。抱歉有点啰嗦。对梯形使用 2 三角形扇形而不是四边形。

谢谢!

替代文字

glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

// Prep the color array. This is the same for all trapezoids.
// 4 verts * 4 components/color = 16 values. 
GLfloat colors[16];
colors[0] = 0.0;
colors[1] = 0.0;
colors[2] = 1.0;
colors[3] = 1.0;
colors[4] = 0.0;
colors[5] = 0.0;
colors[6] = 1.0;
colors[7] = 1.0;

colors[8] = 1.0;
colors[9] = 1.0;
colors[10] = 1.0;
colors[11] = 1.0;
colors[12] = 1.0;
colors[13] = 1.0;
colors[14] = 1.0;
colors[15] = 1.0;

// Draw the trapezoidal frame areas. Each one is two triangle fans.
// Fan of 2 triangles = 4 verts = 8 values
GLfloat vertices[8];

float insetOffset = 100;
float frameMaxDimension = 1000;

// Bottom 
vertices[0] = 0;
vertices[1] = 0;
vertices[2] = frameMaxDimension;
vertices[3] = 0;
vertices[4] = frameMaxDimension - insetOffset;
vertices[5] = 0 + insetOffset;
vertices[6] = 0 + insetOffset;
vertices[7] = 0 + insetOffset;
glVertexPointer(2, GL_FLOAT , 0, vertices); 
glColorPointer(4, GL_FLOAT, 0, colors);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);    

// Left
vertices[0] = 0;
vertices[1] = frameMaxDimension;
vertices[2] = 0;
vertices[3] = 0;
vertices[4] = 0 + insetOffset;
vertices[5] = 0 + insetOffset;
vertices[6] = 0 + insetOffset;
vertices[7] = frameMaxDimension - inset;    
glVertexPointer(2, GL_FLOAT , 0, vertices); 
glColorPointer(4, GL_FLOAT, 0, colors);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);    

/* top & right would be as expected... */

glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

I am doing some really basic experiments around some 2D work in GL. I'm trying to draw a "picture frame" around an rectangular area. I'd like for the frame to have a consistent gradient all the way around, and so I'm constructing it with geometry that looks like four quads, one on each side of the frame, tapered in to make trapezoids that effectively have miter joins.

The vert coords are the same on the "inner" and "outer" rectangles, and the colors are the same for all inner and all outer as well, so I'd expect to see perfect blending at the edges.

But notice in the image below how there appears to be a "seam" in the corner of the join that's lighter than it should be.

I feel like I'm missing something conceptually in the math that explains this. Is this artifact somehow a result of the gradient slope? If I change all the colors to opaque blue (say), I get a perfect solid blue frame as expected.

Update: Code added below. Sorry kinda verbose. Using 2-triangle fans for the trapezoids instead of quads.

Thanks!

alt text

glClearColor(1.0, 1.0, 1.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);

// Prep the color array. This is the same for all trapezoids.
// 4 verts * 4 components/color = 16 values. 
GLfloat colors[16];
colors[0] = 0.0;
colors[1] = 0.0;
colors[2] = 1.0;
colors[3] = 1.0;
colors[4] = 0.0;
colors[5] = 0.0;
colors[6] = 1.0;
colors[7] = 1.0;

colors[8] = 1.0;
colors[9] = 1.0;
colors[10] = 1.0;
colors[11] = 1.0;
colors[12] = 1.0;
colors[13] = 1.0;
colors[14] = 1.0;
colors[15] = 1.0;

// Draw the trapezoidal frame areas. Each one is two triangle fans.
// Fan of 2 triangles = 4 verts = 8 values
GLfloat vertices[8];

float insetOffset = 100;
float frameMaxDimension = 1000;

// Bottom 
vertices[0] = 0;
vertices[1] = 0;
vertices[2] = frameMaxDimension;
vertices[3] = 0;
vertices[4] = frameMaxDimension - insetOffset;
vertices[5] = 0 + insetOffset;
vertices[6] = 0 + insetOffset;
vertices[7] = 0 + insetOffset;
glVertexPointer(2, GL_FLOAT , 0, vertices); 
glColorPointer(4, GL_FLOAT, 0, colors);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);    

// Left
vertices[0] = 0;
vertices[1] = frameMaxDimension;
vertices[2] = 0;
vertices[3] = 0;
vertices[4] = 0 + insetOffset;
vertices[5] = 0 + insetOffset;
vertices[6] = 0 + insetOffset;
vertices[7] = frameMaxDimension - inset;    
glVertexPointer(2, GL_FLOAT , 0, vertices); 
glColorPointer(4, GL_FLOAT, 0, colors);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);    

/* top & right would be as expected... */

glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

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

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

发布评论

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

评论(3

等风来 2024-10-16 19:08:14

正如@Newbie 在评论中所说,

@quixoto:在画图程序中打开图像,用填充工具单击接缝处的某处,您会看到它在那里形成 90 度角线...意味着只有 1 种颜色,“接缝”中的任何地方都没有更亮的颜色。这只是一个幻觉。

真的。虽然我不熟悉 OpenGL 下的这部分数学,但我相信这是如何在三角形顶点之间执行颜色插值的隐式结果......我确信它被称为“双线性插值”。

那么该怎么做才能解决这个问题呢?一种可能性是使用纹理并仅绘制一个纹理四边形(或多个纹理四边形)。

但是,在片段着色器中生成这样的边框应该很容易。

使用 GLSL 着色器的一个不错的解决方案...


假设您正在绘制一个矩形,其左下角的纹理坐标等于 (0,0),右上角的纹理坐标等于 (1,1)。

然后,如果我是正确的,那么在片段着色器中按程序生成“斜接”将如下所示:

varying vec2 coord;

uniform vec2 insetWidth; // width of the border in %, max would be 0.5

void main() {

    vec3 borderColor = vec3(0,0,1);
    vec3 backgroundColor = vec3(1,1,1); 

    // x and y inset, 0..1, 1 means border, 0 means centre

    vec2 insets = max(-coord + insetWidth, vec2(0,0)) / insetWidth;

如果到目前为止我是正确的,那么现在对于每个像素,insets.x 的值都有一个[0..1] 范围内的值

确定给定点水平进入边界的深度,
insets.y 具有相似的垂直深度值。

左侧竖条有 insets.y == 0,
底部水平条有 insets.x = 0,,左下角有一对 (insets.x, insets.y) 覆盖从 (0,0) 到 ( 1,1)。请看图片以清楚起见:

insets

现在我们想要一个转换,对于给定的 (x,y) 对,它将给我们 ONE值[0..1]确定如何混合背景和前景色。 1 表示 100% 边框,0 表示 0% 边框。这可以通过多种方式来完成!

该函数应遵循以下要求:

  • 如果 x==0 且 y==0,则为 0
  • ;如果 x==1 或 y==1
  • 之间的值平滑,则为 1。

假设这样的功能:

float bias = max(insets.x,insets.y);

它满足这些要求。实际上,我非常确定这个函数会给你带来与上面相同的“锐利”优势。尝试在纸上计算左下矩形内的坐标选择。

如果我们想要在那里有一个光滑的圆形斜角,我们只需要这里另一个函数。我认为这样的东西就足够了:

float bias = min( length(insets) , 1 );

这里的 length() 函数只是 sqrt(insets.x*insets.x + insets.y*insets.y) 。重要的是:这转化为:“我们距离边界越远(就欧几里德距离而言),边界应该越明显”,而 min() 只是为了使结果不大于 1 (= 100%)。

请注意,我们的原始函数遵循完全相同的定义 - 但距离是根据棋盘(切比雪夫)度量计算的,而不是欧几里得度量。

这意味着,例如,使用曼哈顿公制,您将拥有第三种可能的斜接形状!它的定义如下:

float bias = min(insets.x+insets.y, 1);

我预测这个也会有一条可见的“对角线”,但对角线将在另一个方向(“\”)。

好的,所以对于其余的代码,当我们有偏差 [0..1] 时,我们只需要混合背景和前景色:

    vec3 finalColor = mix(borderColor, backgroundColor, bias);
    gl_FragColor = vec4(finalColor, 1); // return the calculated RGB, and set alpha to 1 
}

就是这样!将 GLSL 与 OpenGL 结合使用使生活变得更简单。希望有帮助!

As @Newbie posted in the comments,

@quixoto: open your image in Paint program, click with fill tool somewhere in the seam, and you see it makes 90 degree angle line there... means theres only 1 color, no brighter anywhere in the "seam". its just an illusion.

True. While I'm not familiar with this part of math under OpenGL, I believe this is the implicit result of how the interpolation of colors between the triangle vertices is performed... I'm positive that it's called "Bilinear interpolation".

So what to do to solve that? One possibility is to use a texture and just draw a textured quad (or several textured quads).

However, it should be easy to generate such a border in a fragment shader.

A nice solution using a GLSL shader...


Assume you're drawing a rectangle with the bottom-left corner having texture coords equal to (0,0), and the top-right corner with (1,1).

Then generating the "miter" procedurally in a fragment shader would look like this, if I'm correct:

varying vec2 coord;

uniform vec2 insetWidth; // width of the border in %, max would be 0.5

void main() {

    vec3 borderColor = vec3(0,0,1);
    vec3 backgroundColor = vec3(1,1,1); 

    // x and y inset, 0..1, 1 means border, 0 means centre

    vec2 insets = max(-coord + insetWidth, vec2(0,0)) / insetWidth;

If I'm correct so far, then now for every pixel the value of insets.x has a value in the range [0..1]

determining how deep a given point is into the border horizontally,
and insets.y has the similar value for vertical depth.

The left vertical bar has insets.y == 0,
the bottom horizontal bar has insets.x = 0,, and the lower-left corner has the pair (insets.x, insets.y) covering the whole 2D range from (0,0) to (1,1). See the pic for clarity:

insets

Now we want a transformation which for a given (x,y) pair will give us ONE value [0..1] determining how to mix background and foreground color. 1 means 100% border, 0 means 0% border. And this can be done in several ways!

The function should obey the requirements:

  • 0 if x==0 and y==0
  • 1 if either x==1 or y==1
  • smooth values in between.

Assume such function:

float bias = max(insets.x,insets.y);

It satisfies those requirements. Actually, I'm pretty sure that this function would give you the same "sharp" edge as you have above. Try to calculate it on a paper for a selection of coordinates inside that bottom-left rectangle.

If we want to have a smooth, round miter there, we just need another function here. I think that something like this would be sufficient:

float bias = min( length(insets) , 1 );

The length() function here is just sqrt(insets.x*insets.x + insets.y*insets.y). What's important: This translates to: "the farther away (in terms of Euclidean distance) we are from the border, the more visible the border should be", and the min() is just to make the result not greater than 1 (= 100%).

Note that our original function adheres to exactly the same definition - but the distance is calculated according to the Chessboard (Chebyshev) metric, not the Euclidean metric.

This implies that using, for example, Manhattan metric instead, you'd have a third possible miter shape! It would be defined like this:

float bias = min(insets.x+insets.y, 1);

I predict that this one would also have a visible "diagonal line", but the diagonal would be in the other direction ("\").

OK, so for the rest of the code, when we have the bias [0..1], we just need to mix the background and foreground color:

    vec3 finalColor = mix(borderColor, backgroundColor, bias);
    gl_FragColor = vec4(finalColor, 1); // return the calculated RGB, and set alpha to 1 
}

And that's it! Using GLSL with OpenGL makes life simpler. Hope that helps!

_失温 2024-10-16 19:08:14

我认为您看到的是马赫带。您的视觉系统对亮度的一阶导数的变化非常敏感。要消除这种效果,您需要模糊强度。如果沿着穿过该区域的扫描线绘制强度,您会看到有两条线在尖角处相交。为了防止视觉系统突出显示该区域,您需要舍入此连接。您可以通过后期处理模糊或在角落添加一些小三角形来实现此目的,以简化过渡。

I think that what you're seeing is a Mach band. Your visual system is very sensitive to changes in the 1st derivative of brightness. To get rid of this effect, you need to blur your intensities. If you plot intensity along a scanline which passes through this region, you'll see that there are two lines which meet at a sharp corner. To keep your visual system from highlighting this area, you'll need to round this join over. You can do this with either a post processing blur or by adding some more small triangles in the corner which ease the transition.

心安伴我暖 2024-10-16 19:08:14

我过去也有过这样的经历,而且它对几何非常敏感。例如,如果您在单独的操作中将它们分别绘制为三角形,而不是绘制为三角形扇形,则问题就不那么严重(或者,至少在我的情况下是这样,它相似但略有不同)。

我还尝试过的一件事是使用正确的合成模式(或 OpenGL 混合)分别绘制三角形,彼此稍微重叠,这样就不会产生效果。我工作过,但最终没有使用它,因为它只是最终产品的一小部分,而且不值得。

很抱歉,我不知道这种影响的根本原因是什么,但是:(

I had that in the past, and it's very sensitive to geometry. For example, if you draw them separately as triangles, in separate operations, instead of as a triangle fan, the problem is less severe (or, at least, it was in my case, which was similar but slightly different).

One thing I also tried is to draw the triangles separately, slightly overlapping onto one another, with a right composition mode (or OpenGL blending) so you don't get the effect. I worked, but I didn't end up using that because it was only a tiny part of the final product, and not worth it.

I'm sorry that I have no idea what is the root cause of this effect, however :(

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