动手实现一个C++软渲染器(一)做一点微小的工作

动手实现一个C++软渲染器(一)做一点微小的工作

学图形学或者说想学习渲染原理,要自己实现一个软渲染器。这种说法不知道最早是谁提出,也不知道是不是这样,但是作为个图形学萌新我确确实实的在实现的过程中学到了很多东西。我的这个文章,不是教程,也不是原理讲解,第一篇文章只是想记录下在图形学道路上成长的轨迹。各位读者如果对文章内容或者代码等各个方面有什么建议,欢迎指出。

首先感谢韦易笑前辈开源出来的700行软渲染器,在做这个渲染器之前,我先是Fork了这个代码,把里面的很多结构体改为类放在单独的文件里面,在这个过程中自己改了一些实现的方法,对我理解数学原理起了巨大的帮助作用。最后在里面添加了简单的平行光照、正反面剔除以及一个可以自由漫游的相机。但是这时对于光栅化的理解还是不够透彻,比如如何绘制Primitive。所以还是得自己从头开始实现,这就是本文的内容了。

想用C++实现一个软件渲染器,类似DX和OpenGL,除了《3D游戏编程大师技巧》,或者什么网站推荐?www.zhihu.com图标

还感谢拳四郎前辈的文章,里面对Bresenham算法和绘制Primitive的原理讲解,对我帮助很大。当然我参考了很多其他前辈的文章以及国外的资料,这里就不一一列举出来了。

拳四郎:如何写一个软渲染(2)-Primitivezhuanlan.zhihu.com图标

框架的选择:

我用的是C++语言,而渲染器是需要一个窗口的,并且提供一个渲染Buffer的方法。SDL2是个不错的选择,使用简单,配置方便,还能跨平台。为了保证自己真正的搞懂原理,我暂时没用其他的第三方库,而且尽量不复制代码,自己实现。如果说代码里面有相似的地方,是我看了很多遍了,不知不觉就写成那样。

SDl2环境搭建:

这部分参考教程,很简单。渲染Buffer用的是sdl texture streaming 像素格式为RGBA32。但是LockTexture之后像素数据必须用memcpy来拷贝,才能到sdl texture中,这个折腾了我好久。

代码部分:

设计模式没有模仿DX或者OpenGL,因为我寻思这个软渲染器本身也写不大,怎么简单怎么来吧。项目结构:



为了方便绘制图形,我创建了一个叫Canvas的类。最基本的绘制图形的函数DrawPixel(),DrawLine()都在里面。绘制出来的像素数据放在PixelData里面,与像素格式对应的Uint32 类型。

画线算法是Bresenham算法,这没什么好说的。原理上简单的讲就是对于-1<斜率<1的线段像素走势趋于水平,每个像素的下一个位置不会往它的上下两边走。而斜率>1或者斜率<-1的线段也不会往它的左右走。示意图:


代码如下:

void Canvas::DrawLine(Color& c, int x0, int y0, int x1, int y1)
{
	if (x0 == x1 && y0 == y1)
	{
		DrawPixel(c, x0, y0);
	}
	else if (x0 == x1)
	{
		// increment direction
		int inc = (y0 < y1) ? 1 : -1;
		for (int y = y0; y != y1; y += inc)
		{
			DrawPixel(c, x0, y);
		}
		DrawPixel(c, x1, y1);
	}
	else if (y0 == y1)
	{
		// increment direction
		int inc = (x0 < x1) ? 1 : -1;
		for (int x = x0; x != x1; x += inc)
		{
			DrawPixel(c, x, y0);
		}
		DrawPixel(c, x1, y1);
	}
	else
	{
		int dx = abs(x1 - x0);
		int dy = abs(y1 - y0);

		// -1 <= Slope <= 1
		if (dx >= dy)
		{
			// Accumulation
			int acc = 0;
			// increment direction
			int incX = (x0 > x1) ? -1 : 1;
			int incY = (y0 > y1) ? -1 : 1;
			for (int x = x0, y = y0; x != x1 && y != y1; x += incX)
			{
				DrawPixel(c, x, y);
				// Accumulate short side
				acc += dy;
				if (acc >= dx)
				{
					acc -= dx;
					y += incY;
				}
			}
			DrawPixel(c, x1, y1);
		}
		// Slope > 1 || Slope < -1
		else
		{
			// Accumulation
			int acc = 0;
			// increment direction
			int incX = (x0 > x1) ? -1 : 1;
			int incY = (y0 > y1) ? -1 : 1;
			for (int x = x0, y = y0; x != x1 && y != y1; y += incY)
			{
				DrawPixel(c, x, y);
				// Accumulate short side
				acc += dx;
				if (acc >= dy)
				{
					acc -= dy;
					x += incX;
				}
			}
			DrawPixel(c, x1, y1);
		}
	}
}

因为每个像素坐标都是整数,因此,坐标点计算可以全部为整形,前面已经将线段分为两种趋势,因此每种趋势都有一个方向是累加1的,但是另一个方向可能在几个像素之后才会加1。为了避免这种浮点运算,将起止点的水平和垂直高度差的绝对值dx和dy,做自加,假设dy自加大于dx了,那么像素坐标就在垂直方向加1,并且将dy-dx。

			for (int x = x0, y = y0; x != x1 && y != y1; y += incY)
			{
				DrawPixel(c, x, y);
				// Accumulate short side
				acc += dx;
				if (acc >= dy)
				{
					acc -= dy;
					x += incX;
				}
			}

这是相似三角形的原理,我第一次看见的时候,没看懂这种做法,但是想明白之后觉得这种算法非常精妙。正应了前辈们所说的,图形学里面有非常多的hack和trick。

最后就是随机绘制一些线段。代码如下:

int main(int argc, char* args[])
{
	if (!init())
	{
		printf("Failed to initialize!\n");
	}

	else
	{
		// Rendering loop
		bool bQuit = false;
		while (!bQuit)
		{
			SDL_Event event;
			while (SDL_PollEvent(&event))
			{
				if (event.type == SDL_QUIT)
				{
					bQuit = true;
				}
			}

			Color c(Random::GetUint32());
			cav->DrawLine(c, cav->width * Random::GetNormalizedFloat(), cav->height * Random::GetNormalizedFloat(), cav->width * Random::GetNormalizedFloat(), cav->height * Random::GetNormalizedFloat());


			SDL_RenderClear(gRenderer);
			void* mPixels;
			int pitch = 0;
 			SDL_LockTexture(gRenderTarget,nullptr, &mPixels, &pitch);
			memcpy(mPixels, cav->PixelData, cav->width * 4 * cav->height);
			SDL_UnlockTexture(gRenderTarget);

			SDL_RenderCopy(gRenderer, gRenderTarget, nullptr, &cav->rect);

			SDL_RenderPresent(gRenderer);
		}
	}

	close();

	return 0;
}


到这里,渲染器的基本框架就算是搭建好了,做的都是一些很简单的工作,跟真正的图形学还搭不上边。下一步,绘制Primitive才算是进入到图形学领域。这部分我还没实现。就留给下一篇文章了。

渲染器的地址在这里:

AskaTien/Bulbasaurgithub.com图标

不知道有没有读者注意到,渲染器的名字叫做Bulbasaur,其实就是妙蛙种子啦,我那天晚上刚好看了《大侦探皮卡丘》,最可爱的还是妙蛙种子,希望有一天我能渲染出电影里面的效果吧,拜拜!

编辑于 2019-05-26

文章被以下专栏收录