Closed hrkalona closed 1 year ago
oh I'm sorry, I've just seen this now. Let me look into this.
Okay, so I see what you mean. I made another test against a regular color fill and it seems like it is expected that you see the outline.
g2d.setColor(Color.WHITE);
g2d.drawPolygon(triangle);
g2d.setColor(Color.BLUE);
g2d.fillPolygon(triangle);
whereas the barycentric gradient fill produces the following:
g2d.setColor(Color.WHITE);
g2d.drawPolygon(triangle);
g2d.setPaint(new BarycentricGradientPaint(p1, p2, p3, color1, color2, color3));
g2d.fillPolygon(triangle);
However, it appears that the right edge is thicker, meaning that the gradient fill extends like 1 pixel less to the right than the solid color fill. Here's a zoomed in comparison:
I need to look into this more deeply. It could be that it was implemented like this on purpose, so that multiple triangles can be put next to each other seamlessly (each connecting on one edge of the other) for triangle meshes.
Okay I did some more testing. Two things to note:
int[] xPoints = { 50, 350, 200};
int[] yPoints = { 50, 50, 350};
int[] xPointsCC = {350, 50, 200};
int[] yPointsCC = { 50, 50, 350 };
Polygon triangle = new Polygon(xPoints, yPoints, 3); g2d.setColor(Color.WHITE); g2d.drawPolygon(triangle);
Polygon triangleCC = new Polygon(xPointsCC, yPointsCC, 3); g2d.setColor(Color.BLUE); g2d.drawPolygon(triangleCC);
![winding_difference](https://user-images.githubusercontent.com/2974361/231903152-950a41a4-2143-4014-a1a4-586160f28923.png)
2. `BarycentricGradientPaint` currently has a half-pixel shift in coordinates, which is debatable.
https://github.com/hageldave/JPlotter/blob/878bb5fdca6e9e3c2b13265789e99af2cc3f55b2/jplotter/src/main/java/hageldave/jplotter/util/BarycentricGradientPaint.java#L268-L274
![shift-vs-noshift](https://user-images.githubusercontent.com/2974361/231900505-d4d6278f-a648-4df7-a208-da61b7547850.png)
On the right side is the result without the `+.5f` shift. Left and right edge are consistent with the solid color fill in this case, but the top edge of the gradient fill is now fuzzy. The fuzzy edge is probably an artifact due to numeric inaccuracies, a very minor subpixel shift, e.g. `+.001f` could do the trick. This needs some more thinking and testing though. And if this will be done, the sample positions for the multi sampling anti aliasing (MSAA) need to be made consistent.
@hrkalona : all of this is probably not helping with your problem though. Since the regular solid color fill
g2d.setColor(Color.WHITE);
g2d.drawPolygon(triangle);
g2d.setColor(Color.BLUE);
g2d.fillPolygon(triangle);
also does not cover the polygon lines perfectly, I'm not so sure if there will be a satisfactory solution for your use case. The problem seems to be rooted in the way polylines are drawn and how line drawing differs from area filling. Maybe there is a workaround for your problem?
For the BarycentricGradientPaint
you could shift the Point2D
coordinates by +.499f
to get consistent with the solid color fill behavior. You can also try to grow the triangle very slightly for the filling.
The actual problem appeared when I wanted to use BarycentricGradientPaint on a mesh of triangles.
The original mesh with each triangle rendered with a single color, using graphics2d fill method was working ok. When I tried to use the gradient paint, I was observing lines between the triangles.
I will post an image in a couple of days, so you can see the actual issue, since I am currently off for Easter vacations.
Thanks for taking the time to look into it!
Ah I see. You need to use g2d.fillRect(x,y,w,h)
because fillPolygon(Polygon)
or fill(Shape)
will perform clipping on the paint which can result in edges being cut off slightly.
See this part of the triangle renderer: https://github.com/hageldave/JPlotter/blob/14ef91f1b63dca2711d998dfe0c673f9150579dc/jplotter/src/main/java/hageldave/jplotter/renderers/TrianglesRenderer.java#L282-L288
Thanks I will test it. If I got it correctly even though you fill a rectangle, because you use the BarycentricGradientPaint, you only fill with color only a triangle inside the rectangle?
Exactly. Also, there needs to be some testing done with the mentioned subpixel shift if this will allow seamless filling with polygons.
@lvcarx if you have time for this, could you do some tests with 0.001f
shift instead of 0.5f
and fillPolygon(...)
instead of fillRect(..)
?
Had a brief look at it and it looks like it's better with a shift of 0.001f (no artifacts at the top & same line width on the right/left side).
Smaller values seem to lead to the artifacts at the top again.
cool, if you could you also try a mesh of triangles and see if there are obvious seams between the triangles that would be great. Until now the code always used fillRect() with a rectangle slightly larger than the triangle. Maybe with the change in subpixel shift, we can also use fillPolygon() and still have seamless connections between adjacent triangles.
It worked great with fillRect. The meshing is seamless:
package hageldave.jplotter;
import hageldave.imagingkit.core.util.ImageFrame;
import hageldave.jplotter.util.BarycentricGradientPaint;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.io.File;
public class BarycentricTest {
public static void main(String[] args) throws Exception {
// Create a new BufferedImage object
int width = 800;
int height = 800;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// Get the Graphics2D object
Graphics2D g2d = image.createGraphics();
// Define three vertices of the triangle
int[] xPoints = {50, 350, 200};
int[] yPoints = {50, 50, 350};
int[] xPoints2 = {350, 200, 500};
int[] yPoints2 = {50, 350, 350};
int[] xPoints3 = {350, 650, 500};
int[] yPoints3 = {50, 50, 350};
int[] xPoints4 = {-100, 50, 200};
int[] yPoints4 = {350, 50, 350};
int[] xPoints5 = {200, 500, 350};
int[] yPoints5 = {350, 350, 650};
int[] xPoints6 = {500, 650, 350};
int[] yPoints6 = {350, 650, 650};
// Define three colors for the gradient
Color color1 = Color.RED;
Color color2 = Color.GREEN;
Color color3 = Color.BLUE;
// Draw the triangle with a linear gradient
Polygon triangle = new Polygon(xPoints, yPoints, 3);
Point2D p1 = new Point2D.Float(xPoints[0], yPoints[0]);
Point2D p2 = new Point2D.Float(xPoints[1], yPoints[1]);
Point2D p3 = new Point2D.Float(xPoints[2], yPoints[2]);
Polygon triangle2 = new Polygon(xPoints2, yPoints2, 3);
Point2D p21 = new Point2D.Float(xPoints2[0], yPoints2[0]);
Point2D p22 = new Point2D.Float(xPoints2[1], yPoints2[1]);
Point2D p23 = new Point2D.Float(xPoints2[2], yPoints2[2]);
Polygon triangle3 = new Polygon(xPoints3, yPoints3, 3);
Point2D p31 = new Point2D.Float(xPoints3[0], yPoints3[0]);
Point2D p32 = new Point2D.Float(xPoints3[1], yPoints3[1]);
Point2D p33 = new Point2D.Float(xPoints3[2], yPoints3[2]);
Polygon triangle4 = new Polygon(xPoints4, yPoints4, 3);
Point2D p41 = new Point2D.Float(xPoints4[0], yPoints4[0]);
Point2D p42 = new Point2D.Float(xPoints4[1], yPoints4[1]);
Point2D p43 = new Point2D.Float(xPoints4[2], yPoints4[2]);
Polygon triangle5 = new Polygon(xPoints5, yPoints5, 3);
Point2D p51 = new Point2D.Float(xPoints5[0], yPoints5[0]);
Point2D p52 = new Point2D.Float(xPoints5[1], yPoints5[1]);
Point2D p53 = new Point2D.Float(xPoints5[2], yPoints5[2]);
Polygon triangle6 = new Polygon(xPoints6, yPoints6, 3);
Point2D p61 = new Point2D.Float(xPoints6[0], yPoints6[0]);
Point2D p62 = new Point2D.Float(xPoints6[1], yPoints6[1]);
Point2D p63 = new Point2D.Float(xPoints6[2], yPoints6[2]);
g2d.setColor(Color.WHITE);
//g2d.drawPolygon(triangle);
g2d.setPaint(new BarycentricGradientPaint(p1, p2, p3, color1, color2, color3));
g2d.fillPolygon(triangle);
g2d.setPaint(new BarycentricGradientPaint(p21, p22, p23, color2, color3, color1));
g2d.fillPolygon(triangle2);
g2d.setPaint(new BarycentricGradientPaint(p31, p32, p33, color2, color3, color1));
g2d.fillPolygon(triangle3);
g2d.setPaint(new BarycentricGradientPaint(p41, p42, p43, color2, color1, color3));
g2d.fillPolygon(triangle4);
g2d.setPaint(new BarycentricGradientPaint(p51, p52, p53, color3, color1, color2));
g2d.fillPolygon(triangle5);
g2d.setPaint(new BarycentricGradientPaint(p61, p62, p63, color1, color3, color2));
g2d.fillPolygon(triangle6);
// Save the image to a file
ImageFrame.display(image);
File output = new File("triangle.png");
ImageIO.write(image, "png", output);
}
}
With rotation:
Without rotation:
Corresponding code:
public class BarycentricTest {
public static void main(String[] args) throws Exception {
// Create a new BufferedImage object
int width = 800;
int height = 800;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// Get the Graphics2D object
Graphics2D g2d = image.createGraphics();
AffineTransform at = new AffineTransform();
at.rotate(Math.toRadians(10));
// Define three vertices of the triangle
int[] xPoints = {50, 350, 200};
int[] yPoints = {50, 50, 350};
int[] xPoints2 = {350, 200, 500};
int[] yPoints2 = {50, 350, 350};
int[] xPoints3 = {350, 650, 500};
int[] yPoints3 = {50, 50, 350};
int[] xPoints4 = {-100, 50, 200};
int[] yPoints4 = {350, 50, 350};
int[] xPoints5 = {200, 500, 350};
int[] yPoints5 = {350, 350, 650};
int[] xPoints6 = {500, 650, 350};
int[] yPoints6 = {350, 650, 650};
// Define three colors for the gradient
Color color1 = Color.RED;
Color color2 = Color.GREEN;
Color color3 = Color.BLUE;
// Triangle 1
drawTriangle(xPoints, yPoints, color1, color2, color3, at, g2d);
// Triangle 2
drawTriangle(xPoints2, yPoints2, color2, color3, color1, at, g2d);
// Triangle 3
drawTriangle(xPoints3, yPoints3, color2, color3, color1, at, g2d);
// Triangle 4
drawTriangle(xPoints4, yPoints4, color2, color1, color3, at, g2d);
// Triangle 5
drawTriangle(xPoints5, yPoints5, color3, color1, color2, at, g2d);
// Triangle 6
drawTriangle(xPoints6, yPoints6, color1, color3, color2, at, g2d);
// Save the image to a file
ImageFrame.display(image);
File output = new File("triangle.png");
ImageIO.write(image, "png", output);
}
public static void drawTriangle(int[] xPoints, int[] yPoints, Color color1, Color color2, Color color3, AffineTransform at, Graphics2D g2d) {
g2d.setColor(Color.WHITE);
int[][] transformedCoords = transformCoords(xPoints, yPoints, at);
int[] xPointsDouble = transformedCoords[0];
int[] yPointsDouble = transformedCoords[1];
Polygon triangle = new Polygon(xPoints, yPoints, 3);
Point2D p1 = new Point2D.Float((float) xPointsDouble[0], (float) yPointsDouble[0]);
Point2D p2 = new Point2D.Float((float) xPointsDouble[1], (float) yPointsDouble[1]);
Point2D p3 = new Point2D.Float((float) xPointsDouble[2], (float) yPointsDouble[2]);
g2d.setPaint(new BarycentricGradientPaint(p1, p2, p3, color1, color2, color3));
g2d.fillPolygon(triangle);
}
public static int[][] transformCoords(int[] inputCoordsX, int[] inputCoordsY, AffineTransform at) {
List<Double> xPointsList = Arrays.stream(inputCoordsX).asDoubleStream().boxed().collect(Collectors.toList());
List<Double> yPointsList = Arrays.stream(inputCoordsY).asDoubleStream().boxed().collect(Collectors.toList());
double[] mergedArray = new double[inputCoordsX.length + inputCoordsY.length];
for (int i = 0; i < mergedArray.length; i++) {
if (i % 2 == 0) {
mergedArray[i] = xPointsList.get(0);
xPointsList.remove(0);
} else {
mergedArray[i] = yPointsList.get(0);
yPointsList.remove(0);
}
}
double[] pointsDoubleDest = new double[mergedArray.length];
// Coord transformation happens here
at.transform(mergedArray, 0, pointsDoubleDest, 0, inputCoordsX.length);
int x = 0;
int y = 0;
for (int i=0; i<mergedArray.length; i++){
if (i % 2 == 0) {
inputCoordsX[x] = (int) pointsDoubleDest[i];
x++;
} else {
inputCoordsY[y] = (int) pointsDoubleDest[i];
y++;
}
}
return new int[][]{inputCoordsX, inputCoordsY};
}
}
Alright, thank you @lvcarx for further looking into this. We see that the polygon clipping is still interfering and results in visible seams between adjacent triangles despite the smaller subpixel shift. This means that the gradient paint is not perfectly compatible with polygon filling in its current state, so we will continue to use filling with triangle enclosing rectangles.
Using this, you can see that a white line appears, even if the fill takes place after the polygonFill public class TriangleExample {