CG1 - 基本绘图单元

02 Jul 2015

0、概述

软件渲染器制作过程有三个阶段:

市面上人们常用的 3D 图形引擎效果不一,但都支持这些基本的功能和过程。3D 引擎的主要目标是封装实现细节,抽象出逻辑接口,让引擎用户加快开发速度,减少 idea 到产品化的时间。

学习和使用一套成熟的引擎制作东西是正确的,但对于程序员,仿制一个轮子也很有意义。打个比方,一个不了解轮胎性能的赛车手,是很难在不同的场地保持优势的,甚至有可能是极其危险的。

学习并制作一个软件渲染器,当驾车上路时,也就会明白“when and where to speed up”。

1、Windows GDI(Graphics Device Interface)

Windows 提供了 Direct2D/3D、GDI/GDI+ 等多种图形 API。

Direct2D/3D 是微软为 2D/3D 游戏专门优化的 API,其本身就是完整的图形引擎。

GDI/GDI+ 是 Windows 系统最基本的图形设备接口。

为了把图像在 windows 中显示出来,必须调用图形 API。在第一版的程序中,我选择 GDI 作为基本的绘图接口主要出于以下考虑:1)GDI 本身不存在性能瓶颈,也可以支持像素级的访问;2)Direct2D/3D 就是图形引擎,从图形引擎中取出一个绘图单元去构建一个图形引擎,总感觉有些别捏;3)使用 GDI 制作出的图形引擎,到后期可以与 Direct3D 进行比较,效果拔城。

2、寻找 pixel

再次翻出图形学的“渲染说”:

给我一段显存,我可以绘出整个世界。

基于 GDI,有多种操作像素的方式,常用的有 GetPixel()/SetPixel() 和 DIB/DDB。在《Windows 图形编程》7-4节结尾,描述了这几种方式的操作像素性能区别:

考虑到图形引擎需要实时变化,其间每一帧都有大量的像素被重绘,当然是越快越好了。

DIBSection 的创建与像素操作很简单,在我的 Github 中有一份使用 DIBSection 像素操作实现的颜色渐变程序

range

在这份代码中,除去 Windows 程序的框架部分,从 89 行开始,逐个像素进行渲染。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
hbitmap = CreateDIBSection(NULL, Info, DIB_RGB_COLORS, (void**)&pRgb, NULL, NULL);
//从 Info 的信息创建一个 hbitmap 的区块,将 pRgb 指向区块的首地址。

BytesPerLine = cxClient * 3;
if (BytesPerLine % 4 != 0)
	BytesPerLine += 4 - BytesPerLine % 4;
//获取区块显示区域的行宽并对齐

for (float y = 0; y < cyClient; y++) {
	point = pRgb;
	for (float x = 0; x < cxClient; x++) {
		point[0] = 0;
		point[1] = y / cyClient * 255;
		point[2] = x / cxClient * 255;
		point += 3;
	}
	pRgb += BytesPerLine;
}
//point[0..2] 依次对应了 Blue, Green, Red,共同构成了 24 位色的一个 pixel

hdcmem = CreateCompatibleDC(hdc);
SelectObject(hdcmem, hbitmap);
BitBlt(ps.hdc, 0, 0, cxClient, cyClient, hdcmem, 0, 0, SRCCOPY);
//根据当前窗口区域创建一个设备兼容的上下文,并关联到我们创建的 hbitmap 区块
//执行 BitBlt 时,在临时创建的设备上下文中的信息,即刻复制到了当前显示窗口中

DIBSection 的操作过程可以理解为“创建 -> 修改 -> 显示(映射)”。如果给上述代码中的 hdcmem 的名字换成“帧”,或许更好理解,操作过程变为“创建帧 -> 修改帧 -> 显示下一帧”。

3、封装像素操作

既然已经了解 DIBSection 的创建与操作,那么将操作封装起来,也就有了我们自己的 SetPixel()。

我 Github 中的渲染引擎文件在 https://github.com/plinx/CG 目录下。

在这个目录下有 4 个文件夹:

这几篇文章主要讲软件渲染器的内容,但这里安利一下 Milo Yip 的博文《用JavaScript玩转计算机图形学(一)光线追踪入门》、《用JavaScript玩转计算机图形学(二)基本光源》。

在无任何基础的情况下,阅读 Milo Yip 的在知乎上《如何用 C 语言画一个“心形”》的回答和这两篇文章极大地触动了我:原来编程的表达可以这么“精致”。

所以我也满怀敬意地写了人生第一张渲染图和 SoftRender。

以下回归主题。

封装像素操作时,我做了两个工作,抽离 Windows 框架的代码和封装像素操作。

此时软件渲染器中包含了 5 个文件:

如果你已经熟悉了 Windows 编程,可以略过大部分 LWin.h/LWin.cpp 的函数,所有的渲染草走都放在 LWindow::Render 中,而渲染实现则放在 inc 目录下的其他头文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inline void Painter::_setColor(PBYTE& pixel, Color& color)
{
	pixel[0] = color.B;
	pixel[1] = color.G;
	pixel[2] = color.R;
}

inline void Painter::drawPixel(int x, int y, Color& color)
{
	if (y < 0 || y > _height) return;
	if (x < 0 || x > _width) return;
	_pixel = _scanLine + _bytesPerLine * (y);
	_pixel += x * 3;
	_setColor(_pixel, color);
}

借助这个 drawPixel 就可以进行简单的绘图了。以下显示了一个画线的 demo,在这个 demo 中,从坐标 P1(100, 100) 到 P2(400, 400) 绘制了一条白色直线。

1
2
3
4
5
6
7
8
9
void LWindow::LineDemo()
{
	for (int i = 100, j = 100; i < 400; i++, j++)
		painter.drawPixel(i, j, Color(ColorStyle::White));

	BitBlt(_hdc,
		0, 0, _width, _height,
		_hdcMem, 0, 0, SRCCOPY);
}

pixeldemo

基于点,我们就有了创造万物的力量,由点及线,由线及面,就可以创建出 2D 空间的图形了。这里我暂时略去了边界检测部分的内容,等到 3D 裁剪的章节再进行详解。如果你初学至此,可以思考/测试一下,当 drawPixel 超出屏幕边界时会发生什么情况,如何去避免这种情况发生。


如果你发现我文章中的问题或希望与我交流,可以给我发邮件:plinux@qq.com

如果你读后有些许收获,也可以到我的 Github 点赞支持

© 2015 plinx