如何在Windows上快速实现无条带的四边形双线性插值?
高效无条带四边形双线性插值绘制方案(Windows 10兼容,100Hz帧率)
需求与现状
- 核心需求:绘制矩形,每个像素由四个顶点颜色双线性插值生成,要求Windows 10兼容、帧率≥100Hz
- 当前实现:通过
GradientFill将四边形拆分为两个三角形绘制,代码如下:
GradientFill(*pDC, m_arrVert.GetData(), m_arrVert.GetSize(), m_arrTri.GetData(), m_arrTri.GetSize(), GRADIENT_FILL_TRIANGLE);
- 现存问题:三角形共享斜边处频繁出现条带伪影(artifacts)
已尝试方案及问题
- 手动双线性插值
- 效果:完全消除条带
- 缺陷:
SetPixelV逐像素调用系统开销极大,帧率无法达标 - 代码:
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)); } }
- GDI+ PathGradientBrush
- 缺陷:两条对角线均出现条带,且绘制速度无法满足帧率要求
- 代码:
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());
- StretchBlt缩放2×2位图
- 缺陷:设置
HALFTONE模式后仍仅显示四个大像素,无插值效果
- 缺陷:设置
高效无条带解决方案推荐
方案1:内存位图+预计算扫描线(优化手动插值)
放弃逐像素SetPixelV调用,改为直接操作DIB(设备无关位图)的内存缓冲区,通过预计算减少重复运算,大幅提升速度:
- 创建兼容内存DC与DIB位图,锁定像素缓冲区
- 预计算每条y扫描线的顶部/底部颜色插值参数
- 批量填充像素缓冲区后,一次性
BitBlt到目标DC
核心实现代码:
CPaintDC dc(this); CRect rc; GetClientRect(rc); int w = rc.Width(), h = rc.Height(); // 创建DIB位图(24位RGB,顶部为原点) BITMAPINFO bmi = {0}; bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); bmi.bmiHeader.biWidth = w; bmi.bmiHeader.biHeight = -h; bmi.bmiHeader.biPlanes = 1; bmi.bmiHeader.biBitCount = 24; bmi.bmiHeader.biCompression = BI_RGB; BYTE* pBits = nullptr; HBITMAP hBmp = CreateDIBSection(dc, &bmi, DIB_RGB_COLORS, (void**)&pBits, nullptr, 0); HDC memDC = CreateCompatibleDC(dc); HBITMAP oldBmp = (HBITMAP)SelectObject(memDC, hBmp); struct COLOR { int r, g, b; }; COLOR vc[4] = { {241, 188, 82}, {104, 11, 211}, {174, 51, 196}, {10, 199, 247} }; // 逐行计算并填充像素 for (int y = 0; y < h; y++) { double sy = (double)y / (h - 1); // 用h-1避免边缘溢出 // 计算当前行对应的顶部、底部插值颜色 COLOR topCol = { (int)Round(vc[0].r + (vc[1].r - vc[0].r) * sy), (int)Round(vc[0].g + (vc[1].g - vc[0].g) * sy), (int)Round(vc[0].b + (vc[1].b - vc[0].b) * sy) }; COLOR bottomCol = { (int)Round(vc[2].r + (vc[3].r - vc[2].r) * sy), (int)Round(vc[2].g + (vc[3].g - vc[2].g) * sy), (int)Round(vc[2].b + (vc[3].b - vc[2].b) * sy) }; // 填充当前行的每个像素(DIB为BGR顺序) BYTE* pRow = pBits + y * w * 3; for (int x = 0; x < w; x++) { double sx = (double)x / (w - 1); int r = (int)Round(topCol.r + (bottomCol.r - topCol.r) * sx); int g = (int)Round(topCol.g + (bottomCol.g - topCol.g) * sx); int b = (int)Round(topCol.b + (bottomCol.b - topCol.b) * sx); pRow[x*3 + 0] = (BYTE)b; pRow[x*3 + 1] = (BYTE)g; pRow[x*3 + 2] = (BYTE)r; } } // 一次性绘制到目标DC BitBlt(dc, 0, 0, w, h, memDC, 0, 0, SRCCOPY); // 清理资源 SelectObject(memDC, oldBmp); DeleteObject(hBmp); DeleteDC(memDC);
- 优势:直接操作内存缓冲区,避免系统调用开销;预计算减少重复运算,帧率可轻松突破100Hz;完全消除条带伪影
- 兼容性:原生支持Windows 10,无需升级工具链
方案2:GDI AlphaBlend结合渐变位图(可选)
创建两个水平渐变位图,通过垂直方向的AlphaBlend实现双线性插值,效果接近方案1,但实现复杂度略高。
关键注意事项
- 绝对避免
SetPixelV等逐像素GDI调用,这类调用存在大量系统开销,无法满足高帧率需求 - 插值计算时使用
h-1和w-1作为分母,避免边缘像素插值溢出 - 操作DIB时注意像素顺序为BGR而非RGB
内容的提问来源于stack exchange,提问作者Chris Korda
相关产品推荐
相关产品推荐

