Margoo

...?

EasyX 的三种绘图抗锯齿方法 金牌收录

前言

本文中的抗锯齿方法分为三个部分,难度由易到难,性能也由低效到高效,效果也由一般到精致,读者可以根据自己的需求选择适合自己的方法。

本文所有代码均在 VS2022 + EasyX20220610 下通过编译并确定效果正常。

方法一:自己手写抗锯齿算法(以 SSAA 抗锯齿算法举例)

SSAA 抗锯齿,即为“超级采样抗锯齿”,其实现方法可以简单形容为“先在在一个比原来画布大 x 倍的新画布上绘制有锯齿的内容,将新画布插值缩小并贴回原画布”,下图展示了 SSAA 是如何进行抗锯齿的。

SSAA 图片描述

SSAA 大致原理图

  • SSAA 的优点
  1. 便于实现。
  2. 可以只通过 EasyX 和部分 WinAPI 就可以实现,和 EasyX 本身 API 搭配。
  • SSAA 的缺点
  1. N 倍的缩放会导致性能较低
  2. 缩放倍数过小无法达到较好的抗锯齿效果

这里我写了一段实例代码来实现 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+ 实现的优点
  1.  系统自带 API 方便使用。
  2.  EasyX 原本兼容 GDI+ 和 GDI 无需过多操作。
  3.  简洁明了易用,只要简单封装就可以和 EasyX 无缝交互。
  • 调用 GDI+ 实现的缺点
  1.  GDI+ 本身效率过低,渲染过慢。
  2.  一般新手学习难度可能较大。

方法三:调用 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 进行交互的优点
  1.  性能极佳,GPU 绘图硬件加速,不用担心性能问题。
  2.  功能强大,还可以和 D3D 进行交互。
  3.  系统自带 API 方便随时调用。
  • 调用 D2D 和 EasyX 进行交互的缺点
  1.  已经超出了新手的学习范围。

评论 (1) -

  • 好耶,能调用DirectX就不怕性能了!

添加评论