EasyX 的三种绘图抗锯齿方法
前言
本文中的抗锯齿方法分为三个部分,难度由易到难,性能也由低效到高效,效果也由一般到精致,读者可以根据自己的需求选择适合自己的方法。
本文所有代码均在 VS2022 + EasyX20220610 下通过编译并确定效果正常。
# 方法一:自己手写抗锯齿算法(以 SSAA 抗锯齿算法举例) SSAA 抗锯齿,即为“超级采样抗锯齿”,其实现方法可以简单形容为“先在在一个比原来画布大 x 倍的新画布上绘制有锯齿的内容,将新画布插值缩小并贴回原画布”,下图展示了 SSAA 是如何进行抗锯齿的。
SSAA 大致原理图
- SSAA 的优点
- 便于实现。
- 可以只通过 EasyX 和部分 WinAPI 就可以实现,和 EasyX 本身 API 搭配。
- SSAA 的缺点
- N 倍的缩放会导致性能较低
- 缩放倍数过小无法达到较好的抗锯齿效果
这里我写了一段实例代码来实现 SSAA 算法,缩放我直接调用了 WinAPI 的 StretchBlt 函数来实现:
#include <graphics.h>
#include <conio.h>
// 缩放贴图(直接调用 StretchBlt 函数实现)
void PutImage(IMAGE* TargetImage, int x, int y, IMAGE* SourceImage, int TargetWidth, int TargetHeight)
{
HDC TargetDC = GetImageHDC(TargetImage);
HDC SourceDC = GetImageHDC(SourceImage);
int Width = SourceImage->getwidth();
int Height = SourceImage->getheight();
SetStretchBltMode(TargetDC, HALFTONE);
StretchBlt(TargetDC, x, y, TargetWidth, TargetHeight, SourceDC, 0, 0, Width, Height, SRCCOPY);
}
// SSAAx4 抗锯齿实现画圆
void SSAAx4DrawCircle(int X1, int Y1, int X2, int Y2)
{
LINESTYLE Style;
getlinestyle(&Style);
Style.thickness *= 4;
int SourceWidth = (X2 - X1) + 2;
int SourceHeight = (Y2 - Y1) + 2;
// 创建四倍的 SSAA 画布
IMAGE* LineImage = new IMAGE((X2 - X1) * 4 + Style.thickness * 2, (Y2 - Y1) * 4 + Style.thickness * 2);
SetWorkingImage(LineImage);
// 调整线条粗细也为四倍
setlinestyle(&Style);
// 绘制 n 倍的图形
ellipse(5, 5, LineImage->getwidth() - 5, LineImage->getheight() - 5);
SetWorkingImage();
PutImage(NULL, X1, Y1, LineImage, SourceWidth, SourceHeight);
// 释放内存
delete LineImage;
}
// SSAAx4 抗锯齿实现画线
void SSAAx4DrawLine(int X1, int Y1, int X2, int Y2)
{
LINESTYLE Style;
getlinestyle(&Style);
Style.thickness *= 4;
int SourceWidth = (X2 - X1) + 2;
int SourceHeight = (Y2 - Y1) + 2;
// 创建四倍的 SSAA 画布
IMAGE* LineImage = new IMAGE((X2 - X1) * 4 + Style.thickness * 2, (Y2 - Y1) * 4 + Style.thickness * 2);
SetWorkingImage(LineImage);
// 调整线条粗细也为四倍
setlinestyle(&Style);
// 绘制 n 倍的图形
line(5, 5, LineImage->getwidth() - 5, LineImage->getheight() - 5);
SetWorkingImage();
PutImage(NULL, X1, Y1, LineImage, SourceWidth, SourceHeight);
// 释放内存
delete LineImage;
}
int main()
{
initgraph(340, 206);
SetWindowText(GetHWnd(), L"SSAAx4 抗锯齿演示");
// 设置线条粗细使抗锯齿更为明显
setlinestyle(PS_SOLID, 4);
line(0, 0, 80, 80);
SSAAx4DrawLine(200, 0, 280, 80);
outtextxy(0, 80, L"无抗锯齿画线");
outtextxy(200, 80, L"SSAAX4 抗锯齿画线");
ellipse(0, 100, 80, 180);
SSAAx4DrawCircle(200, 100, 280, 180);
outtextxy(0, 184, L"无抗锯齿画圆");
outtextxy(200, 184, L"SSAAX4 抗锯齿画圆");
return _getch();
}
实现的效果图如下。
如上图可见 SSAAx4 算法的效果并不理想,其实这也并不是 SSAA 的问题,而是插值器的问题,大家可以通过修改插值器(即为缩放贴图)的实现以获得更好的 SSAA 抗锯齿效果,下面有一张图片对比了由 SSAA 算法和无 SSAA 算法绘制的圆细节上的区别。
上图为无 SSAA 下图为有 SSAA
(SSAA 算法还有一个加强版 - MSAA 算法,具体原理就是在边缘附近进行多几次采样,各位有兴趣可以去自己参考实现)
方法二:调用系统 GDI+ API 进行绘图
GDI+ 是 WinAPI 中的一个绘图 API,它是 GDI 的一个加强版,因为 EasyX 底层由 GDI 和 GDI+ 构成,所以直接调用 GDI+ 的 API 进行绘图也是兼容 EasyX 窗口的,关于 GDI+ 的资料,可以看微软官方提供的说明文档,写的很好,可以快速入门,这里我主要介绍 GDI+ 和 EasyX 交互的方法。
GDI+ 和 EasyX 交互的时候,必须将 EasyX 的批量绘图模式打开,否则 GDI+ 画上去的内容将会被刷掉,GDI+ 中的 Graphics 可以直接通过 HDC 来构造,具体样例代码如下:
#include <graphics.h>
#include <conio.h>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
int main()
{
initgraph(340, 206);
SetWindowText(GetHWnd(), L"GDI+ 抗锯齿演示");
BeginBatchDraw();
setlinestyle(PS_SOLID, 4);
// 启动 GDI+
Gdiplus::GdiplusStartupInput Input;
ULONG_PTR Token;
Gdiplus::GdiplusStartup(&Token, &Input, NULL);
Gdiplus::Graphics Graphics(GetImageHDC());
Gdiplus::Pen Pen(Gdiplus::Color(255, 255, 255), 4.f);
// 设置绘图质量为高质量
Graphics.SetSmoothingMode(Gdiplus::SmoothingMode::
SmoothingModeHighQuality);
// 调用 GDI+ 绘图
Graphics.DrawLine(&Pen, 200, 0, 280, 80);
Graphics.DrawEllipse(&Pen, Gdiplus::Rect{ 200, 100, 80, 80 });
// EasyX 对比绘图
line(0, 0, 80, 80);
ellipse(0, 100, 80, 180);
outtextxy(0, 80, L"无抗锯齿画线");
outtextxy(200, 80, L"SSAAX4 抗锯齿画线");
outtextxy(0, 184, L"无抗锯齿画圆");
outtextxy(200, 184, L"GDI+ 抗锯齿画圆");
// 关闭 GDI+
Gdiplus::GdiplusShutdown(Token);
// 以约为 60fps 的帧率更新界面
// 注:因为这里已经写入了 EasyX 的界面 buffer,所以关闭 Gdiplus 也是可以显示内容的
while (true)
{
FlushBatchDraw();
Sleep(14);
}
EndBatchDraw();
return 0;
}
下图是 GDI+ 的绘图结果
- 调用 GDI+ 实现的优点
- 系统自带 API 方便使用。
- EasyX 原本兼容 GDI+ 和 GDI 无需过多操作。
- 简洁明了易用,只要简单封装就可以和 EasyX 无缝交互。
- 调用 GDI+ 实现的缺点
- GDI+ 本身效率过低,渲染过慢。
- 一般新手学习难度可能较大。
方法三:调用 Direct2D API 进行绘图(支持 GPU 硬件加速)
Direct2D 是 Direct API 的 2D 版本,它功能更加地强大,同样的,如果你对 D2D 没有一定了解,可以观看这篇微软的入门教程,非常简单易学,这里我同样只介绍交互的方法。
由于 EasyX 是由 GDI+ 和 GDI 构成的界面,所以 D2D 不能直接绘制(虽然是有兼容 API),所以我的兼容思路是:EasyX 开启批量绘图模式,并用 CreateDCRenderTarget 来创建一个 DC Render Target 类,并用 GetImageHDC 和 BindDC 来实现绑定 DC,这样就可以和 EasyX API 保持一个良好的兼容,然后当调用完了 D2D 的 API 时,再使用 FlushBatchDraw 方法来显示渲染内容,下面是我的实现代码:
#include <graphics.h>
#include <d2d1.h>
#include <wincodec.h>
#include <stdio.h>
#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "dwrite.lib")
// D2D 对象的安全释放
template <class T> void DxObjectSafeRelease(T** ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
int main()
{
// 创建 EasyX 窗口
initgraph(340, 206);
SetWindowText(GetHWnd(), L"D2D 硬件加速抗锯齿演示");
setlinestyle(PS_SOLID, 2);
// 创建 D2D 工厂
ID2D1Factory* Facotry = NULL;
HRESULT ResultHandle = D2D1CreateFactory(
D2D1_FACTORY_TYPE_SINGLE_THREADED,
&Facotry
);
// 创建 DC Render 并指定硬件加速
auto Property = D2D1::RenderTargetProperties(
D2D1_RENDER_TARGET_TYPE::D2D1_RENDER_TARGET_TYPE_HARDWARE,
D2D1::PixelFormat(
DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_IGNORE
), 0.0, 0.0, D2D1_RENDER_TARGET_USAGE_GDI_COMPATIBLE, D2D1_FEATURE_LEVEL_DEFAULT
);
// 创建 EasyX 兼容的 DC Render Target
ID2D1DCRenderTarget* DCRenderTarget;
HRESULT Result = Facotry->CreateDCRenderTarget(
&Property,
&DCRenderTarget
);
// 绑定 EasyX DC
RECT EasyXWindowRect = { 0, 0, 640, 480 };
DCRenderTarget->BindDC(GetImageHDC(), &EasyXWindowRect);
if (FAILED(Result))
{
printf("D2D Facotry Created Failed\n");
return -1;
}
// 创建画笔
ID2D1SolidColorBrush* WhiteBrush = NULL;
DCRenderTarget->CreateSolidColorBrush(
D2D1::ColorF(D2D1::ColorF::White),
&WhiteBrush
);
if (!WhiteBrush)
{
printf("D2D Brush Created Failed\n");
return -1;
}
BeginBatchDraw();
// 设置抗锯齿
DCRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE::D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
// 调用 D2D 进行绘图
DCRenderTarget->BeginDraw();
DCRenderTarget->Clear(D2D_COLOR_F(D3DCOLORVALUE{ 0, 0, 0 }));
DCRenderTarget->DrawLine(D2D1_POINT_2F{ 200, 0 }, D2D1_POINT_2F{ 280, 80 }, WhiteBrush, 2.f);
DCRenderTarget->DrawEllipse(D2D1_ELLIPSE{ D2D1_POINT_2F{ 240, 140 }, 40, 40 }, WhiteBrush, 2.f);
DCRenderTarget->EndDraw();
// EasyX 对比绘图
line(0, 0, 80, 80);
ellipse(0, 100, 80, 180);
outtextxy(0, 80, L"无抗锯齿画线");
outtextxy(165, 80, L"D2D 硬件加速抗锯齿画线");
outtextxy(0, 184, L"无抗锯齿画圆");
outtextxy(165, 184, L"D2D 硬件加速抗锯齿画圆");
// 以约为 60fps 的帧率更新界面
while (true)
{
FlushBatchDraw();
Sleep(14);
}
EndBatchDraw();
// 释放 D2D 对象
DxObjectSafeRelease(&DCRenderTarget);
DxObjectSafeRelease(&WhiteBrush);
DxObjectSafeRelease(&Facotry);
return 0;
}
下图是 D2D 抗锯齿和 EasyX 交互的结果
- 调用 D2D 和 EasyX 进行交互的优点
- 性能极佳,GPU 绘图硬件加速,不用担心性能问题。
- 功能强大,还可以和 D3D 进行交互。
- 系统自带 API 方便随时调用。
- 调用 D2D 和 EasyX 进行交互的缺点
- 已经超出了新手的学习范围。``