mini3DRender

    背景

    草稿纸上推图形学坐标变换 里推了坐标变换的全过程,为了验证和牢固掌握这些变换,这里将会实现一个基础的只使用CPU进行运算的渲染器。

    主要功能

    • 显示obj文件
    • 点、直线、三角形绘制
    • 贴图绘制

    具体实现

    [1] Bresenham 直线绘制算法

    本文介绍著名的Bresenham 直线绘制算法
    由于现在的屏幕是由一个个像素点构成的,所以在计算机中绘制的直线也就是只能是由离散的点构成,Bresenham直线绘制算法可以在屏幕上绘制任何直线。并且它只需要使用整除运算,完全不需要浮点运算,从而具有超高的性能。
    接下来介绍我理解的Bresenham算法流程,并在最后贴出我实现的C++代码。(为了介绍的容易理解,这里只举斜率为0到1之间的直线)
    如下图所示,离散化直线的本质是已知(xi,yi)点时确定下一个点(xi+1,yi+1)的位置:只有两种可能,一种是(xi+1,yi),另一种是(xi+1,yi+1)
    1665406210210
    Bresenham算法的核心就是x相同时观察直线到拟合直线的距离(图中的D),距离D超过中点也就是0.5时,拟合曲线就把自己的y值加一,也就是图中移上去一格,从而始终保持D小于等于0.5。
    伪代码就是

    D=0
    for each x in (x0,x1) 
      DrawPoint(x,y) 
      D+=k 
      if(D>0.5) 
        y++   
        D-=1 //y移上去之后距离也要更新 
    

    这个版本已经可以画任意直线了但是需要浮点运算(比如计算k值,D值)。我们想要更好的性能,于是就有了下面一个优化版本:
    首先,我们不想将D与0.5比较(浮点数比较),可以先将D变大一倍不影响结果,此时伪代码变成:

    D1=0 //用D1表示新的拟合直线与原直线距离
    for each x in (x0,x1) 
      DrawPoint(x,y) 
      D1+=2*k 
      if(D1>1) 
        y++  
        D1-=2  
    

    由于k=dy/dx,dy=y1-y0,dx=x1-x0,且dy,dx都是整数(输入在屏幕上的点坐标x0,y0,x1,y1一定是整数),于是我们将D扩大dx倍,去掉计算D时候的浮点加法运算,此时伪代码变成:

    D2=0 //用D2表示新的拟合直线与原直线距离
    for each x in (x0,x1) 
      DrawPoint(x,y) 
      D2+=2*dy//整数运算! 
      if(D2>dx) 
        y++  
        D2-=2*dx  
    

    于是,我们就得到了一个全部都是整数运算的直线绘制算法!于是它就可以在我之前做的没有浮点运算器的计算机中绘制直线!用Nexys4 DDR 制作一台计算机系列
    以下是完整C++代码:

    void DrawLine_Bresenham(int x0, int y0, int x1, int y1, const Color& color)
    {
    	if (x1 < x0) {
    		int tempX, tempY;
    		tempX = x0;
    		x0 = x1;
    		x1 = tempX;
    		tempY = y0;
    		y0 = y1;
    		y1 = tempY;
    	}
    	if (x1 == x0) {
    		for (int y = min(y0, y1); y <= max(y0, y1); y++) {
    			DrawPoint(x0, y, color);
    		}
    		return;
    	}
    
    	//k=0
    	if (y1==y0) {
    		for (int x = x0; x < x1; x++) {
    			DrawPoint(x, y0, color);
    		}
    		return;
    	}
    	int dx = x1 - x0, dy = y1 - y0;
    	bool kSign = ((y1 - y0) >= 0 && (x1 - x0) >= 0) ? true : false; //true => positive false =>negative
    	bool kGreaterThenOne = (abs(y1 - y0) > abs(x1 - x0)) ? true : false;
    
    	int d = 0;
    	// 0<k<=1
    	if (kSign == true && kGreaterThenOne == false) {
    		int y = y0;
    
    		for (int x = x0; x < x1; x++) {
    			DrawPoint(x, y, color);
    			d += 2 * dy;
    			if (d > dx) {
    				d -= 2 * dx;
    				y++;
    			}
    		}
    	}
    	//k>1
    	else if (kSign == true && kGreaterThenOne == true) {
    		int x = x0;
    
    		for (int y = y0; y < y1; y++) {
    			DrawPoint(x, y, color);
    			d += 2 * dx;
    			if (d > dy) {
    				d -= 2 * dy;
    				x++;
    			}
    		}
    	}
    	//-1<k<=0
    	else if(kSign==false&&kGreaterThenOne==false){
    		int y = y0;
    
    		for (int x = x0; x < x1; x++) {
    			DrawPoint(x, y, color);
    			d += -2 * dy;
    			if (d > dx) {
    				d -= 2 * dx;
    				y--;
    			}
    		}
    	}
    	//k<-1
    	else if (kSign == false && kGreaterThenOne == true) {
    		int x = x0;
    
    		for (int y = y0; y > y1; y--) {
    			DrawPoint(x, y, color);
    			d += 2 * dx;
    			if (d > -dy) {
    				d -= -2 * dy;
    				x++;
    			}
    		}
    	}
    	
    }
    

    附上用Bresenham直线绘制算法画的兔子
    20221011212023

    [2]三角形光栅化算法

    本文介绍三角形光栅化,大多数软光栅都是以绘制三角面片为基础。
    这里介绍经典的拆分法:
    如下图所示,所有三角形可以被分类成这三种:
    1.朝下的两个点y值相同
    2.朝上的两个点y值相同
    3.其它的任意三角形(但是拆分成1和2类型两个三角形)
    1665494158668
    从而,我们可以知道,只要会画1和2这两类三角形,我们就可以通过组合,画出任意三角形!
    接下来就介绍下如何画出第1类三角形。
    1665495793379
    第2类和第1类基本一样,就不写了,直接介绍最后的任意三角形绘制:
    我们发现要想用画1和2类三角形的方法画任意三角形的时候,还缺了一个点,组成两个三角形。所以我们还需要确定第四个点v4的位置:
    IMG_20221011_214705
    1665496210988
    这里的证明都是初中几何知识,简单但是最好自己画一下。
    得到了第4个点的坐标就可以用前面画1和2类三角形的函数画出整个三角形了!

    void DrawTopFlatTriangle(const Vec& v1, const Vec& v2, const Vec& v3, const Color& color)
    {
       double XL = v1.x, XR = v1.x;
       double dX1 = -(v2.x - v1.x) / (v2.y - v1.y), dX2 = -(v3.x - v1.x) / (v3.y - v1.y);
       double ZL = v1.z, ZR = v1.z;
       double dZ1 =- (v2.z - v1.z) / (v2.y - v1.y), dZ2 =- (v3.z - v1.z) / (v3.y - v1.y);
       if (round(v1.y) - round(v2.y) <= 1) {
       	DrawScanLine(XL, ZL, XR, ZR, round(v1.y), color);
       	return;
       }
       for (int y =round( v1.y); y > v2.y; y--)
       {
       	DrawScanLine(XL, ZL, XR, ZR, y, color);
       	XL += dX1;
       	XR += dX2;
       	ZL += dZ1;
       	ZR += dZ2; 
       }
    }
    
    void DrawBottomFlatTriangle(const Vec& v1, const Vec& v2, const Vec& v3, const Color& color)
    {
    
    
       double XL = v1.x, XR = v1.x;
       double dX1 = (v2.x - v1.x) / (v2.y - v1.y), dX2 = (v3.x - v1.x) / (v3.y - v1.y);
       double ZL = v1.z, ZR = v1.z;
       double dZ1 = (v2.z - v1.z) / (v2.y - v1.y), dZ2 = (v3.z - v1.z) / (v3.y - v1.y);
       if (round(v2.y) - round(v1.y) <= 1) {
       	DrawScanLine(XL, ZL, XR, ZR, round(v1.y), color);
       	return;
       }
       for (int y = round(v1.y); y <v2.y; y++)
       {
       	DrawScanLine(XL, ZL, XR, ZR, y, color);
       	XL += dX1;
       	XR += dX2;
       	ZL += dZ1;
       	ZR += dZ2;
       }
    }
    
    void DrawTriangle(const Vec& v1, const Vec& v2, const Vec& v3, const Color& color)
    {
       //按y值从小到大排序3个顶点
       Vec V[3] = { v1,v2,v3 };
       int maxIndex=0, minIndex = 0;
       Vec maxV = v1, minV = v1,midV=v1;
       for (int i = 0; i < 3; i++)
       {
       	if (V[i].y < minV.y)
       	{
       		minIndex = i;
       		minV = V[i];
       	}
       	if (V[i].y > maxV.y)
       	{
       		maxIndex = i;
       		maxV = V[i];
       	}
       }
       int midIndex;
       for (int i = 0; i < 3; i++) {
       	if (i == maxIndex || i == minIndex) {
       		continue;
       	}
       	else {
       		midIndex = i;
       		midV = V[i];
       		
       	}
       }
       Vec V1 = minV, V2 = midV, V3 = maxV;
       V1.x = (int)V1.x; V1.y = (int)V1.y;
       V2.x = (int)V2.x; V2.y = (int)V2.y;
       V3.x = (int)V3.x; V3.y = (int)V3.y;
    
       if (V1.y == V2.y) {
       	 DrawTopFlatTriangle(V3, V1, V2, color);
       }
       else if (V2.y == V3.y) {
       	DrawBottomFlatTriangle(V1, V3, V2, color);
       }
       else {
       	Vec V4;
       	V4.y = V2.y;
       	V4.x = V1.x + (V4.y - V1.y) / (V3.y - V1.y) * (V3.x - V1.x);
       	V4.z = V1.z + (V4.y - V1.y) / (V3.y - V1.y) * (V3.z - V1.z);
    
       	DrawBottomFlatTriangle(V1, V2, V4, color);
       	DrawScanLine(V2.x, V2.z, V4.x, V4.z, V2.y, color);
       	DrawTopFlatTriangle(V3, V2, V4, color);
    
       }
    
    }
    

    下面附上用这个三角形光栅化算法画的兔子!
    20221011215425

    [3] 重心坐标(Barycentric Coordinates)

    重心坐标这里是将三角形内的任意点p的坐标,用三角形的顶点坐标表示出来,形如:p=iA+jB+k*C。

    这样三角形任意点的坐标都可以表示为(i,j,k),对于图形学中需要对三角形内用到插值的各种功能非常有用,如纹理贴图,光照等。

    为了给mini3DRender加上纹理贴图功能,就必须需要先了解三角形重心坐标。
    这个文章首先会介绍三角形重心坐标,最后通过重心坐标完成三角形内插值运算。
    首先先介绍一下直线的重心坐标
    ----_20221026090009

    直线的重心坐标就是将在直线上的任意点用两个端点表示:p=jA+kB (j+k=1),我们用这个结论推一下三角形的任意点重心坐标表示。
    1543e26b7d05714140dcbe196158eb6
    387919aa646439bf287be3d80a60d10

    5b40f8ee50f938f362ff01a472bb1ad
    如上面两张草稿纸推的,我们可以得出任意点的重心坐标中的i,j,然后k就等于1-i-j。
    于是我们就得到二维三角形内任意点的重心坐标。
    但是,我的mini3DRender最后经过了透视投影,坐标发生了变换,我们这里计算的结果也要相应的修正。

    2a8dc94d27e494aa08c62bdb46cffb0
    ff0241b25d1f7507dd22d7155a7bf75

    参考链接

    【GAMES101-现代计算机图形学课程笔记】Lecture 09 Shading 3 (纹理映射)
    图形学基础知识:重心坐标(Barycentric Coordinates)
    深入探索透视纹理映射(上)
    深入探索透视纹理映射(下)

    [4] 纹理映射关于透视投影的矫正

    绘制三角形时需要贴图,贴图需要u、v坐标,u、v坐标需要插值,而相机空间坐标变换到裁剪空间并不能保持线性,整个空间扭曲了!(透视投影)
    所以u、v坐标的插值不能用裁剪空间或者后面的屏幕空间(裁剪空间到屏幕空间的变换是线性的)的坐标进行插值,而要用相机空间的坐标来计算u、v插值。
    这篇文章的目标是将裁剪空间的坐标还原为相机空间坐标。
    这篇文章基本上是对深入探索透视纹理映射(上)深入探索透视纹理映射(下)做的笔记。
    ----_20221028172839
    93c6b687551f92d3a8f370b5d3b00fe

    [5] 光照

    光照,在光栅化渲染器中,是使用近似方法表现类似自然界中的物体光照效果。
    这里会介绍Blinn-Phong和Phong光照模型,基本步骤如下图所示:
    Phong_components_version_4
    这两个模型将整体的光照分为:
    Ambient(环境光)+Diffuse(散射光)+Specular(高光或镜面反射光)

    原理

    1-2
    2
    3-2
    4

    实现

    我将会在mini3DRender里面添加光照功能。

    • 新建light.h 和 light.cpp,对点光源进行定义
    • 在绘制三角形函数中计算当前三角形的法向量,传入到最后的绘制扫描线函数
    • 根据上面的公式计算,计算每个点的光照

    效果

    下面还是斯坦福兔子!(但是加了光照)
    lightBunny

    参考

    初学光栅渲染器的一些参考资料
    入门Shading,详解Blinn-Phong和Phong光照模型

    参考链接

    绘制直线的光栅化算法