开发者

Gradient "miter" in OpenGL shows seams at the join

开发者 https://www.devze.com 2023-02-01 11:57 出处:网络
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 wa

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.开发者_StackOverflow社区 Using 2-triangle fans for the trapezoids instead of quads.

Thanks!

Gradient "miter" in OpenGL shows seams at the join

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);


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:

Gradient "miter" in OpenGL shows seams at the join

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!


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.


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 :(

0

精彩评论

暂无评论...
验证码 换一张
取 消