0

I want to draw a rectangle such that each of its pixels are interpolated from the colors specified at its four corners, and it needs to be fast. I'm currently doing it by drawing two triangles, using a fairly ancient Windows API called GradientFill, like so:

GradientFill(*pDC, m_arrVert.GetData(), m_arrVert.GetSize(), 
    m_arrTri.GetData(), m_arrTri.GetSize(), GRADIENT_FILL_TRIANGLE);

Unfortunately I sometimes observe undesirable banding effects along the shared hypotenuse of the two triangles, depending on which colors are used at the vertices. An image demonstrating the issue is shown below.

However I do this, it needs to compatible with Windows 10 and fast enough that I can do it at 100 Hz. A Direct2D gradient patch might work, though it's overkill as I don't need curved geometry, and I can't test that at the moment, as I'm using Visual Studio 2012. Direct3D, possibly with a custom shader, would likely work, but would be messy to say the least. GDI+ has gradient support, but I expect it would be too slow. Both Direct2D and Direct3D are significantly more complex than GradientFill, and involve tool upgrades, steep learning curves, and major impact on my application.

Before I wade into all those challenges, a reality check: is the banding theoretically avoidable? I'm inclined to think so, because I notice it always appears along the diagonal from top left to bottom right, which is also the shared hypotenuse of my two triangles, and never along the other diagonal. I don't see how this could be coincidental. But is it an inherent consequence of using triangles? In other words, is it reasonable to expect interpolating a quad to look the same as dividing a quad into two triangles and interpolating them separately?

quad via two gradient triangles

EDIT: I rolled my own rectangular linear interpolation, fed it the same four color values, and the diagonal line went away, as shown below. I conclude that it's not reasonable to expect interpolating a quad to look the same as dividing a quad into two triangles and interpolating them separately. So what's the fastest way to do rectangular interpolation in Windows?

my custom quad linear interpolation

To be clear, the above image is the desired result. Here's my (unacceptably slow) quad interpolation code, which does two horizontal lerps for top and bottom, and then a vertical lerp between the result of the top and bottom lerps, repeated for each color channel:

CPaintDC dc(this); // device context for painting
CRect   rc;
GetClientRect(rc);
int w = rc.Width();
int h = rc.Height();
struct COLOR { int r; int g; int b; };
COLOR   vc[4] = {   // vertex colors
    {241, 188, 82}, // top left
    {104, 11, 211}, // top right
    {174, 51, 196}, // bottom left
    {10, 199, 247}, // bottom right
};
for (int y = 0; y < h; y++) {
    for (int x = 0; x < w; x++) {
        double  sx = double(x) / w;
        double  sy = double(y) / h;
        double  rt = vc[0].r + (vc[1].r - vc[0].r) * sx;
        double  rb = vc[2].r + (vc[3].r - vc[2].r) * sx;
        int r = Round(rt + (rb - rt) * sy);
        double  gt = vc[0].g + (vc[1].g - vc[0].g) * sx;
        double  gb = vc[2].g + (vc[3].g - vc[2].g) * sx;
        int g = Round(gt + (gb - gt) * sy);
        double  bt = vc[0].b + (vc[1].b - vc[0].b) * sx;
        double  bb = vc[2].b + (vc[3].b - vc[2].b) * sx;
        int b = Round(bt + (bb - bt) * sy);
        dc.SetPixelV(x, y, RGB(r, g, b));
    }
}

EDIT2: I tried GDIPlus PathGradientBrush out of curiosity, and was surprised to learn that it also exhibits banding, along both diagonals. This isn't visually acceptable, even if it were fast enough, which it isn't. YakovGalka's suggestion to StretchBlt a 2 x 2 bitmap doesn't work: despite setting HALFTONE mode, I just get four huge pixels. So it looks like shader language is the only way.

GDIPlus PathGradientBrush

My GDIPlus code is as follows:

CPaintDC dc(this);
Graphics g(dc); 
CRect   rc;
GetClientRect(rc);
GraphicsPath    gp;
gp.AddRectangle(Rect(0, 0, rc.Width(), rc.Height()));
Color   arCol[4];
arCol[0] = Color(241, 188, 82);
arCol[1] = Color(104, 11, 211);
arCol[2] = Color(10, 199, 247);
arCol[3] = Color(174, 51, 196);
int ar = Round((arCol[0].GetR() + arCol[1].GetR() + arCol[2].GetR() + arCol[3].GetR()) / 4.0);
int ag = Round((arCol[0].GetG() + arCol[1].GetG() + arCol[2].GetG() + arCol[3].GetG()) / 4.0);
int ab = Round((arCol[0].GetB() + arCol[1].GetB() + arCol[2].GetB() + arCol[3].GetB()) / 4.0);
PathGradientBrush   br(&gp);
br.SetCenterColor(Color(ar, ag, ab));
int nCols = 4;
br.SetSurroundColors(arCol, &nCols);
g.FillRectangle(&br, 0, 0, rc.Width(), rc.Height());
  • I don't really see unexpected artifact in your drawings, what is seen just looks fairly (mathematically) expected. Direct2D (using shader undercovers) will only bring you a Linear Gradient Brush in that matter wich may be equivalent https://learn.microsoft.com/en-us/windows/win32/direct2d/how-to-create-a-linear-gradient-brush it's quite easy to at least test though starting from official sample https://learn.microsoft.com/en-us/samples/microsoft/windows-classic-samples/simple-direct-2d-application/ – Simon Mourier Mar 27 '23 at 21:54
  • @SimonMourier Thanks. But you do see the glowing fuzzy line from the top left to bottom right in both images yes? My point is that no matter what colors I choose, I NEVER see the opposite diagonal appear. And, the shared hypotenuse happens to be top left to bottom right. So I'm alleging that if did an explicit quad interpolation (barycentric, pixel by pixel) with those same colors, it would be slow, but the line would go away. I aim to prove this later today. – victimofleisure Mar 27 '23 at 21:59
  • 1
    Your 'quad interpolation' is actually called **bilinear interpolation**. And it is indeed different from barycentric interpolation done on two triangles separately. The closest thing I can think of in GDI is to use [StretchBlt with HALFTONE](https://stackoverflow.com/q/4250738/277176) and a 2x2 source image. – Yakov Galka Mar 28 '23 at 01:48
  • @YakovGalka Thanks for that nice idea! I did something similar in Direct3D once. I will try it ASAP and see how it performs. I seem to recall that StretchBlt is often exceedingly slow, especially when the destination rectangle is large. But we'll see. – victimofleisure Mar 28 '23 at 01:51
  • @victimofleisure - so yes, your "own rectangular linear interpolation" is not really a linear interpolation. With what's builtin in Direct2D, you don't have that, you only have Linear or Radial Gradiant Brush (same as with WPF, UWP, Visual Composition https://learn.microsoft.com/en-us/windows/uwp/composition/composition-brushes or WinUI3 as they are all built on the same underlying techs) – Simon Mourier Mar 28 '23 at 07:16
  • @SimonMourier My method consists of three linear interpolations, like a sideways ‘H’: two horizontal lerps for top and bottom, and then a vertical lerp between the result of the top and bottom lerps, repeated for each color channel. My result is what a 2D color picker would be expected to show, it appears my question is possibly a duplicate of https://stackoverflow.com/questions/13210998/opengl-colour-interpolation/13211349#13211349 and according to what I read there, shader language is the recommended way to do this quickly. StretchBlt on a 2x2 bitmap will likely be too slow. – victimofleisure Mar 28 '23 at 08:50
  • @YakovGalka I tried your StretchBlt idea, but I couldn’t get it to work. Despite setting the stretch mode to HALFTONE, I just get four enormous pixels. – victimofleisure Mar 28 '23 at 12:28
  • Try using the triangular gradient, but just subdividing the rectangle into more triangles -- 4 or 8 all meeting in the center, like a pizza. It won't be exactly the same as your bilinear interpolation, but there might not be any visible artifacts. – Matt Timmermans Mar 28 '23 at 18:50
  • @MattTimmermans I did try that, and it doesn't help much unless the triangle count is a lot larger than 8. This seems kludge-y. I want to see it reliably look as good as my quad bilinear interpolator. Appearance matters, as it's for an art project. But speed also matters. It's a tough nut. Frankly I'm surprised I have to go to such lengths. OK not that surprised. You can see images of the art project here: https://sourceforge.net/projects/triplight/ – victimofleisure Mar 28 '23 at 19:48
  • 1
    It sounds like you're going to have to write a shader – Matt Timmermans Mar 28 '23 at 19:58
  • @MattTimmermans It would be my first shader, a steep learning curve no doubt. But before I try it, I’d like to have more confidence that what I’m trying to do is in fact possible in shader language. I had the impression that GPU manufacturers had abandoned quads and were only supporting triangles. But I could be wildly misunderstanding. – victimofleisure Mar 28 '23 at 20:13
  • It's very possible, and in fact is one of the easiest shaders you could write. It doesn't matter that your geometry is reduced the triangles. Each execution of your shader would color just one pixel, given its "texture coordinates". https://learn.microsoft.com/en-us/windows/win32/direct3d11/pixel-shader-stage – Matt Timmermans Mar 28 '23 at 20:55

0 Answers0