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?
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?
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.
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());