[libgdx] Drawing gradient with vertices producing diagonal line artefact

I’m drawing a gradient using the SpriteBatch method draw(Texture, float[], int, int) that takes an array of vertices and other data alongside a texture and renders it to screen. I set each corner to a specific colour, and it renders it pretty well. However, I’ve noticed an artefact of sorts. There’s a line going bottom-left to top-right that doesn’t “seem” correct.

On the coloured (red green blue white) gradient, you don’t really notice it. If you turn your head and look at it on your monitor from an angle, it becomes extremely visible. You can also see the lighting on the blocks how there’s lines going top-left-to-bottom-right making it look jagged.

The gradient code is a modified version of davedes horizontal/vertical code here made to fit with four colours instead of only two. I am not using ShapeRenderer because its alpha blending (even with blending turned on) doesn’t exactly work very well.
Gradient code:

private static float[] gradientverts = new float[20];

	public static void drawGradient(SpriteBatch batch, float x, float y, float width, float height,
			Color bl, Color br, Color tr, Color tl) {
		int idx = 0;
		gradientverts[idx++] = x;
		gradientverts[idx++] = y;
		gradientverts[idx++] = bl.toFloatBits(); // bottom left
		gradientverts[idx++] = filltexRegion.getU(); //NOTE: texture coords origin is top left
		gradientverts[idx++] = filltexRegion.getV2();

		gradientverts[idx++] = x;
		gradientverts[idx++] = y + height;
		gradientverts[idx++] = tl.toFloatBits(); // top left
		gradientverts[idx++] = filltexRegion.getU();
		gradientverts[idx++] = filltexRegion.getV();

		gradientverts[idx++] = x + width;
		gradientverts[idx++] = y + height;
		gradientverts[idx++] = tr.toFloatBits(); // top right
		gradientverts[idx++] = filltexRegion.getU2();
		gradientverts[idx++] = filltexRegion.getV();

		gradientverts[idx++] = x + width;
		gradientverts[idx++] = y;
		gradientverts[idx++] = br.toFloatBits(); // bottom right
		gradientverts[idx++] = filltexRegion.getU2();
		gradientverts[idx++] = filltexRegion.getV2();

		batch.draw(filltexRegion.getTexture(), gradientverts, 0, gradientverts.length);
	}

filltexRegion is a TextureRegion that contains a Texture of a 1x1 white square (generated by a pixmap at runtime).

Thanks!

Seems to be because your square is made of 2 triangles. One is red white and blue, the other is blue green and red. There’s no mixing happening between the green and white. You could try getting it to draw as 4 triangles instead, each taking one side of the square with an additional vertex in the center of the square. The center vertex would probably be colored as the average color of the 4 corners.

I did what you said (4 triangles, avg. colour in the middle). It’s definitely better, but now there are visible lines on both directions, showing where each triangle’s boundary is.

Code now:

	private static float[] gradientverts = new float[20];
	private static Color tempGradientColor = new Color();

	public static void drawGradient(SpriteBatch batch, float x, float y, float width, float height,
			Color bl, Color br, Color tr, Color tl) {
		tempGradientColor.set((bl.r + br.r + tr.r + tl.r) / 4f, (bl.g + br.g + tr.g + tl.g) / 4f,
				(bl.b + br.b + tr.b + tl.b) / 4f, (bl.a + br.a + tr.a + tl.a) / 4f);
		int idx = 0;
		
		// draw bottom face
		idx = 0;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		
		gradientverts[idx++] = x;
		gradientverts[idx++] = y;
		gradientverts[idx++] = bl.toFloatBits(); // bottom left
		gradientverts[idx++] = filltexRegion.getU(); //NOTE: texture coords origin is top left
		gradientverts[idx++] = filltexRegion.getV2();
		
		gradientverts[idx++] = x + width;
		gradientverts[idx++] = y;
		gradientverts[idx++] = br.toFloatBits(); // bottom right
		gradientverts[idx++] = filltexRegion.getU2();
		gradientverts[idx++] = filltexRegion.getV2();
		
		batch.draw(filltexRegion.getTexture(), gradientverts, 0, gradientverts.length);

		// draw top face
		idx = 0;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		
		gradientverts[idx++] = x;
		gradientverts[idx++] = y + height;
		gradientverts[idx++] = tl.toFloatBits(); // top left
		gradientverts[idx++] = filltexRegion.getU();
		gradientverts[idx++] = filltexRegion.getV();

		gradientverts[idx++] = x + width;
		gradientverts[idx++] = y + height;
		gradientverts[idx++] = tr.toFloatBits(); // top right
		gradientverts[idx++] = filltexRegion.getU2();
		gradientverts[idx++] = filltexRegion.getV();
		
		batch.draw(filltexRegion.getTexture(), gradientverts, 0, gradientverts.length);

		// draw left face
		idx = 0;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		
		gradientverts[idx++] = x;
		gradientverts[idx++] = y + height;
		gradientverts[idx++] = tl.toFloatBits(); // top left
		gradientverts[idx++] = filltexRegion.getU();
		gradientverts[idx++] = filltexRegion.getV();
		
		gradientverts[idx++] = x;
		gradientverts[idx++] = y;
		gradientverts[idx++] = bl.toFloatBits(); // bottom left
		gradientverts[idx++] = filltexRegion.getU(); //NOTE: texture coords origin is top left
		gradientverts[idx++] = filltexRegion.getV2();
		
		batch.draw(filltexRegion.getTexture(), gradientverts, 0, gradientverts.length);

		// draw right face
		idx = 0;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = x + (width / 2);
		gradientverts[idx++] = y + (height / 2);
		gradientverts[idx++] = tempGradientColor.toFloatBits(); // middle
		gradientverts[idx++] = 0.5f;
		gradientverts[idx++] = 0.5f;
		
		gradientverts[idx++] = x + width;
		gradientverts[idx++] = y + height;
		gradientverts[idx++] = tr.toFloatBits(); // top right
		gradientverts[idx++] = filltexRegion.getU2();
		gradientverts[idx++] = filltexRegion.getV();
		
		gradientverts[idx++] = x + width;
		gradientverts[idx++] = y;
		gradientverts[idx++] = br.toFloatBits(); // bottom right
		gradientverts[idx++] = filltexRegion.getU2();
		gradientverts[idx++] = filltexRegion.getV2();

		batch.draw(filltexRegion.getTexture(), gradientverts, 0, gradientverts.length);

	}

I had to do a bit of trickery to make sure there were four vertices “per triangle”. This is probably the cause of the creases, isn’t it…

If you zoom in close on your rainbow square, it appears the diagonal lines are the result of many horizontal and vertical bands meeting and forming ‘arrows’ all pointing toward the center of the texture. Dithering/adding noise to the texture helps a little. The shadows under the terrain look initially acceptable, if you’re using this for colored lighting - the artifact might not be much of a problem because lights with that much color variance likely wont be right next to each other very often. Also, once the lights are overlayed onto proper tile textures rather than green and brown squares it will be less noticeable.

I’m going to leave this as-is because as LiquidNitrogen said, actual textures on the blocks will help to not make the effect so visible.

Thanks for your help!

Instead of using triangles, directly render a quad using GL_TRIANGLE_FAN. Then it should be only one shape, and the artifacts will be smaller I think. Not quite sure on this though.

Do you have an example of how I could implement this? I haven’t really worked with lower-level things, mostly with libraries that do the work for me. :stuck_out_tongue:

You could use a gradient texture instead, as bilinear filtering doesn’t have that problem.

I meant that you could simulate (not the right word, correct me) the GL_QUADS which is deprecated using GL_TRIANGLE_FAN like this. I don’t know how LibGDX does OpenGL, but it should be somewhat similar to this.


batcher.begin(Primitive.TRIANGLE_FAN);
{
    batcher.vertex(0, 0);  // Top left corner of quad
    batcher.vertex(w, 0);  // Top right corner of quad
    batcher.vertex(w, h);  // Bottom right corner of quad
    batcher.vertex(0, h);  // Bottom left corner of quad
}
batcher.end();

Then it will draw a QUAD using a TRIANGLE_FAN. The Batcher here is my replacement to the OpenGL’s glBegin/glEnd interface that uses VBOs instead. This should work in my opinion.

I found a class called Mesh. It has vertices and whatnot blah blah blah but you can render it with a ShaderProgram (I’m assuming default shader works?) and a primitive type. I’ll try this out.

SHC, thanks for suggesting the TRIANGLE_FAN! I’ll try it and see what happens!

I tried using a mesh. So far it renders but it’s solid white. I’ll have to come back to this tomorrow; it’s been a long day.

private static float[] gradientverts = new float[16];
	private static Mesh gradientmesh = null;

	public static void drawGradient(SpriteBatch batch, float x, float y, float width, float height,
			Color bl, Color br, Color tr, Color tl) {
		if (gradientmesh == null) {
			gradientmesh = new Mesh(false, 4, 4, new VertexAttribute(Usage.Position, 3,
					"a_position"), new VertexAttribute(Usage.ColorPacked, 4, "a_color"));
			gradientmesh.setIndices(new short[] { 0, 1, 2, 3 });
		}
		
		// bottom left
		gradientverts[0] = convertScreenXToMesh(x);
		gradientverts[1] = convertScreenYToMesh(y);
		gradientverts[2] = 0;
		gradientverts[3] = bl.toFloatBits();
		
		// bottom right
		gradientverts[4] = convertScreenXToMesh(x + width);
		gradientverts[5] = convertScreenYToMesh(y);
		gradientverts[6] = 0;
		gradientverts[7] = br.toFloatBits();
		
		// top right
		gradientverts[8] = convertScreenXToMesh(x + width);
		gradientverts[9] = convertScreenYToMesh(y + height);
		gradientverts[10] = 0;
		gradientverts[11] = tr.toFloatBits();
		
		// top left
		gradientverts[12] = convertScreenXToMesh(x);
		gradientverts[13] = convertScreenYToMesh(y + height);
		gradientverts[14] = 0;
		gradientverts[15] = tl.toFloatBits();
		
		gradientmesh.setVertices(gradientverts);
		
		gradientmesh.render(defaultShader, GL20.GL_TRIANGLE_FAN, 0, gradientverts.length);

	}
	
	public static float convertScreenXToMesh(float x){
		return (x / Settings.DEFAULT_WIDTH) * 2 - 1;
	}
	
	public static float convertScreenYToMesh(float y){
		return (y / Settings.DEFAULT_HEIGHT) * 2 - 1;
	}