Polygon corners with GL_LINE

I’m making a rectangle with:

        glPolygonMode(GL_FRONT_AND_BACK,GL_LINE);
        GL11.glBegin(GL_POLYGON); 
        //top line
        glVertex2f(mX, mY);
        glVertex2f(mX2, mY);        
        //right line
        //glVertex2f(mX2, mY);
        glVertex2f(mX2, mY2);
        //bottom line
        //glVertex2f(mX2, mY2);
        glVertex2f(mX, mY2);        
        //left line
       // glVertex2f(mX, mY2);
        //glVertex2f(mX, mY);
        glEnd();

My corners don’t act like proper connecting corners. If I have the shape on GL_FILL, the corners look totally right. If I have it on GL_LINE, the corners of the lines do not totally touch. They stop short of each other on the part that doesn’t overlap so that you have an effect looking sort of like: #. Like so:


.
Red is line, black is width. Even at line width 1f the lines do this.

How can I draw a rectangle with proper corners?

You can compute and render the “caps” of the lines manually, so that they connect properly (there might be one or two computer graphics papers on that), or: Have a look at NanoVG to render proper vector graphics with OpenGL, which is supported by the latest LWJGL 3.1.0 release.

You have to do it manually. Here’s an example:

Can you show me how that is done (manually)? I thought I understood because I’m only making a rectangle, but looking at what you have with all those angles and how it looks proper, I must not fully understand how to do it.

Self calculation is way more prudent than adding a library at this point.

You basically draw lines using triangles to accomplish this. A line is defined by two points and a line width, which in turn forms a quad. To get the result I did, you need extra information. The shape of a line’s corners depends not just on the line itself, but also on the previous and next lines that we’re connecting it to.

So, given four input 2D vertex positions (V1-V4) and a line width, we need to do the following:

  1. Calculate the normalized vectors of each line: L1 = normalize(V2-V1), L2 = normalize(V3-V2), L3 = normalize(V4-V3).

  2. We then calculate the directions of the green lines. This is done by normalizing the average of two line directions and rotating it 90 degrees (= calculate the normal of a line): G1 = rotate90Degrees(normalize(L1+L2)), G2 = rotate90Degrees(normalize(L2+L3)). rotate90Degrees() takes in (x, y) and returns (-y, x).

  3. We now know that the four blue dots we’re looking for are equal to [icode]B1 and B2 = V2 ± G1lineWidth/2 * x[/icode] and [icode]B3 and B4 = V3 ± G2lineWidth/2 * y[/icode].

If we set x and y to 1.0, the system works for straight lines, but as the angles at V2 and V3 get smaller we’ll get thinner and thinner lines, so we need to compensate for that. Let’s imagine some cases and try to come up with how x (and y) should be calculate:

  • If L2 just continues in the exact same direction as L1 (180 degree angle), no compensation is necessary in that case. In this case we want x to be equal to 1.0.
  • If L1 and L2 form a 90 degree corner, it’s clear that the green line needs to cover the hypothenuse of a right triangle. Given two sides of 1 and 1, the hypothenuse becomes sqrt(2), so in this case we want x=sqrt(2).
  • As the angle between L1 and L2 goes towards 0, the green lines become more and more aligned with the red lines, so the compensation will tend to infinity in this case. Hence, it makes sense that a division by 0 occurs.

Let’s try x = 1 / (dot(L1, L2)0.5+0.5). If the two vectors are going in the same direction, the result is 1/(1.00.5+0.5) = 1.0 as it should. For a 0 angle, the dot product is -1: 1/(-10.5+0.5) = 1/0, again as it should. However, for a 90 degree angle, the dot product is zero: 1/(00.5+0.5) = 1/0.5 = 2, which is wrong. We need to pass the divider through sqrt(), which results in:

  1. Calculate x = 1 / sqrt(dot(L1, L2)*0.5+0.5) and y = 1 / sqrt(dot(L2, L3)*0.5+0.5).

  2. At this point, we have everything we need to calculate the four blue points using the formulas in 3. These four points are then used to construct two triangles that form a quad.

Damn yo, that is a hella, hella explanation. Thank you! I feel like I understand most of it but not fully.

Should this
[icode]L1 = normalize(V2-V1), L2 = normalize(V2-V1), L3 = normalize(V2-V1)[/icode] be [icode]L1 = normalize(V2-V1), L2 = normalize(V3-V2), L3 = normalize(V4-V3)[/icode]?

These lines [icode]B1 = V2 ± G1lineWidth/2 * x[/icode] and [icode]B2 = V3 ± G2lineWidth/2 * y[/icode] yield 2 vertex points each?

I may be getting confused at points because I was thinking you were trying to simplify the representation of the math, but V1, L1, and G1 all actually represent Vertex2 data objects? B1 and B2 are both data objects that represent a line made up of the 4 vertices for B11 & B12 and B21 & B22?

Concerning the L1-L4 lines: Yes, copy paste error. I’ve edited my original post.

Concerning B1 and B2: No, you construct 4 vertices due to the ± there. One of B1 is V2 - G1lineWidth/2x and one is V2 + G1lineWidth/2x.

Yes, all values listed except lineWidth, x and y are vectors here.

To be clear, 4 vertices total from those 2 lines? 2 vertices from each line?

Are B1 and B2 a data type? B1 and B2 have 2 vertices each? Like, those lines wouldn’t work as code, or is that syntactically correct?

Sorry, was stupid of me to write “B1=” and “B2=”. I’ve modified the original post again.

As you can see on the image, there are 4 points we’re interested in. They are located at the intersection points of the red lines, and also intersecting the green lines (G1 and G2). The two points that are on G1 are generated by taking V2 and moving in both directions along G1 a certain distance, which is where the ± comes from.

Sorry about that; I hope this clarifies it.

Ah, thank you so much! You have been most helpful. I think I do now understand what I need to do for this. I’ll report back how I do. ;D

Good luck!

[quote=“theagentd,post:5,topic:57845”]
Thanks for your detailed explanation to Optimo AgentD! Enjoyed reading this thread on my lunch break. Its been awhile since I worked with OGL can you remind me why it is important to first normalize the vectors in step 1? This is probably a stupid question from someone who spent the last month looking at cosine angles lol

Two reasons:

  1. You probably already know this, but dot(A, B) = |A|*|B|*cos(angle between A and B). If we normalize A and B, |A| and |B| will both become 1.0, so we end up with just cos(angle), which is what we need in point 4.

  2. When calculating the direction of G1 and G2, we essentially add two line directions together and normalize the result to get the “average” direction. If the two vectors aren’t normalized, the result will be skewed by the length of the two lines. If we have that L1 = (1 000 000, 0) and L2 = (0, 1), we want the result to still be exactly inbetween the two lines (0.707, 0.707). However, normalizing (1 000 000 + 0, 0 + 1) without normalizing the inputs first will massively skew the results as L1 is so much longer, so the result is something like (0.9999…, 0.0000…). This would in turn screw up the calculation for x and y because of the assumptions there.

ahh I see your answer makes perfect sense. I was thinking along the lines that the actual length of a vector was important to create the sharper edges in your example.

I think I see what is causing the confusion for me now. I have been reading a lot of math notation, rather than coding, recently, so the following line of code is ambiguous for me to understand what you are doing.

B1 and B2 <— is this a logical “and”? or do you mean B1 = (V2 ± G1*lineWidth/2 * x) and B1 = B2 ?

The properies of B1 and B3 appear to be undefined. Just very unclear here what you are trying to express, sorry.

also what is “±”, in my mind it looks like a numerator of some kind, maybe a function?

Well sir, I got something, but it ain’t right. Perhaps you can find my mistake? I’m stumped atm.

I’m using this for a dragged selection box. Mouse coordinates (@click and present) are input. It appears to have some of the correct points, but part of it draws off to the origin. I was going to use GL_TRIANGLE_STRIP, but this seemed just as good for testing for now. I could be wrong.

	public void drawDragBox(double mX, double mY, double mX2, double mY2)
	{
    	float lineWidth = 4.6f;
    	
    	Vector2f V1 = new Vector2f((float)mX, (float)mY);
    	Vector2f V2 = new Vector2f((float)mX, (float)mY2);
    	Vector2f V3 = new Vector2f((float)mX2, (float)mY2);
    	Vector2f V4 = new Vector2f((float)mX2, (float)mY);    
    	
    	Vector2f L1 = (V1.subtract(V2)).normalize(); //subtract V1 [b]FROM[/b] V2 and then normalize
    	Vector2f L2 = (V2.subtract(V3)).normalize(); 
    	Vector2f L3 = (V3.subtract(V4)).normalize();
    	Vector2f L4 = (V4.subtract(V1)).normalize();
    	
    	Vector2f G1 = rotate90Degrees(L1.add(L2).normalize());
    	Vector2f G2 = rotate90Degrees(L2.add(L3).normalize());
    	Vector2f G3 = rotate90Degrees(L3.add(L4).normalize());
    	Vector2f G4 = rotate90Degrees(L4.add(L1).normalize());
    	
    	double x1 = 1 / Math.sqrt(L4.dot(L1)*0.5+0.5);
    	double y1 = 1 / Math.sqrt(L1.dot(L2)*0.5+0.5);
    	
    	double x2 = 1 / Math.sqrt(L1.dot(L2)*0.5+0.5); //the line theagentd's example was built for
    	double y2 = 1 / Math.sqrt(L2.dot(L3)*0.5+0.5);
    	
    	double x3 = 1 / Math.sqrt(L2.dot(L3)*0.5+0.5);
    	double y3 = 1 / Math.sqrt(L3.dot(L4)*0.5+0.5);
    	
    	double x4 = 1 / Math.sqrt(L3.dot(L4)*0.5+0.5);
    	double y4 = 1 / Math.sqrt(L4.dot(L1)*0.5+0.5);	

    	
    	//B1 = V2 + G1*lineWidth/2 * x  >> V2.add(G1.scale(lineWidth/2 * x)
    	//B2 = V2 - G1*lineWidth/2 * x
    	//and 
    	//B3 = V3 + G2*lineWidth/2 * y
    	//B4 = V3 - G2*lineWidth/2 * y

    	//assemble vertices for drawing triangles
    	Vector2f A1 = V1.add(G4.scale((float)(lineWidth/2 * x1)));
    	Vector2f A2 = (G4.scale((float)(lineWidth/2 * x1))).subtract(V1); //subtract FROM V1
    	
    	Vector2f A3 = V2.add(G1.scale((float)(lineWidth/2 * y1)));
    	Vector2f A4 = (G1.scale((float)(lineWidth/2 * y1))).subtract(V2);
    	
    	Vector2f B1 = V2.add(G1.scale((float)(lineWidth/2 * x2)));
    	Vector2f B2 = (G1.scale((float)(lineWidth/2 * x2))).subtract(V2);
    	
    	Vector2f B3 = V3.add(G2.scale((float)(lineWidth/2 * y2)));
    	Vector2f B4 = (G2.scale((float)(lineWidth/2 * y2))).subtract(V3);
    	
    	Vector2f C1 = V3.add(G2.scale((float)(lineWidth/2 * x3)));
    	Vector2f C2 = (G2.scale((float)(lineWidth/2 * x3))).subtract(V3);
    	
    	Vector2f C3 = V4.add(G3.scale((float)(lineWidth/2 * y3)));
    	Vector2f C4 = (G3.scale((float)(lineWidth/2 * y3))).subtract(V4);
    	
    	Vector2f D1 = V4.add(G3.scale((float)(lineWidth/2 * x4)));
    	Vector2f D2 = (G3.scale((float)(lineWidth/2 * x4))).subtract(V4);
    	
    	Vector2f D3 = V1.add(G4.scale((float)(lineWidth/2 * y4)));
    	Vector2f D4 = (G4.scale((float)(lineWidth/2 * y4))).subtract(V1);
		
    	glColor3f(1, 0, 0);
        GL11.glLineWidth(lineWidth);
        GL11.glBegin(GL_TRIANGLES); 

        glVertex2f(A1.x, A1.y);
        glVertex2f(A2.x, A2.y);
        glVertex2f(A3.x, A3.y);
        
        glVertex2f(A3.x, A3.y);
        glVertex2f(A4.x, A4.y);
        glVertex2f(A2.x, A2.y);
        
        glVertex2f(B1.x, B1.y);
        glVertex2f(B2.x, B2.y);
        glVertex2f(B3.x, B3.y);
        
        glVertex2f(B3.x, B3.y);
        glVertex2f(B4.x, B4.y);
        glVertex2f(B2.x, B2.y);        
 
        glVertex2f(C1.x, C1.y);
        glVertex2f(C2.x, C2.y);
        glVertex2f(C3.x, C3.y);
        
        glVertex2f(C3.x, C3.y);
        glVertex2f(C4.x, C4.y);
        glVertex2f(C2.x, C2.y);
        
        glVertex2f(D1.x, D1.y);
        glVertex2f(D2.x, D2.y);
        glVertex2f(D3.x, D3.y);
        
        glVertex2f(D3.x, D3.y);
        glVertex2f(D4.x, D4.y);
        glVertex2f(D2.x, D2.y);

        glEnd();
	}
	
	//rotates a vector 90 degrees
	public Vector2f rotate90Degrees(Vector2f vec)
	{
		Vector2f rotVec = new Vector2f(-vec.y, vec.x);
		
		return rotVec;
	}

where is glEnd() ?

Edited my post. I had deleted some irrelevant things from my code for the post and glEnd() got caught in it.

At least show a screenshot so we can see what happens.

[quote]At least show a screenshot so we can see what happens.
[/quote]
I’d love to ;D . Maybe you can help me with this one too? I’m quite new still.

PrintScreen doesn’t work. I looked for a bit but found no ready-to-go screenshot code for LWJGL3 or OpenGL. Any recommendations are welcome. Otherwise, my next goal will be to find and implement screenshot code in my project. Will have a screenshot for you fine folks then. :slight_smile:

I use Fraps personally. You can also easily implement this in code using glReadPixels(). glReadPixels() reads pixels from the currently bound framebuffer (or the window’s backbuffer if you have no FBO bound). Just read pixels back to a ByteBuffer, copy the result to a BufferedImage, write it out using ImageIO.