前言
能够想到自己动手实现一个软渲染器,相信大家都对图形学有所了解,对其中的渲染部分更有一个清晰的认识。你可能用的是OpenGL或者D3D,但是殊途同归,其中的思想是一致的,明白了这个思想,了解渲染管线的流程,自己也可以动手实现一个软渲染器。这个过程是对之前所学知识的一次巩固,并能发现自己在学习过程中遗漏的点,当然,更可贵的是,完成这样一个软渲染器后你会更加坚定自己的发展方向(能够很好的完成它,难道不是真爱?)
开源地址:GitHub/ProEthan/SoftwareRendering
一、渲染管线概述
这是OpenGL渲染管线的一个流程,下面我结合自己实现的软渲染器对这个流程做一个讲解。
1、顶点数据:顶点数据存储在内存中,如何告诉GPU数据在哪?如何让GPU知道传过来的这堆数据是什么?这就涉及到输入处理的问题。关于数据在哪的问题,可以使用C++中的指针来绑定一个内存位置进行标示;关于数据是什么的问题,就可以采用一些描述性的字段啦,OpenGL和D3D11都有各自的实现方式,但是总体还是差不多的,主要是标示这堆顶点数据每个顶点有哪些属性以及每个属性的占用空间和相对开始位置。比如,对于一个三角形:
float triangle[] = {
-0.5, -0.5, 0.0, 255, 0, 0, 1,
0.5, -0.5, 0.0, 0, 255, 0, 1,
0.0, 0.5, 0.0, 0, 0, 255, 1,
};
相信大家能猜到,每行的前3个float是顶点的位置,后四个float是顶点的RGBA值。但是计算机猜不到啊,所以在这个阶段你还需要解释这些数据都是什么。可以使用步长、起始位置等标示。最终可以将他们封装到一个结构体数组中,数组中的每一个结构体元素包含一个顶点的所有属性。当然,你也可以有其他的方式来解释和存储顶点数据。
2、顶点着色器:这是渲染管线的开始位置,经过顶点数据的输入处理阶段后,一段有规律的数据就传到了这里。顶点着色器接收一个顶点的输入。可以做一个循环,遍历结构体数组中的每个顶点。在这个阶段,单独处理每一个顶点数据,主要做顶点的空间变换:
相信学习了一些图形API的你们已经知道怎么做了,其实就是几个矩阵运算,用model矩阵将顶点数据vertex从局部空间变换到世界空间,用view矩阵将从世界空间变换到摄影机空间,再用projection矩阵将其变换到裁剪空间,最后做一个裁剪和透视除法变换到屏幕空间。这其中涉及到几个难点,分别是绕任意轴旋转矩阵的推导、透视投影矩阵的推导之后会详细讲解。
3、图元装配阶段:将空间变换后的一个三角形的三个顶点保存到一个数组中。
4、几何着色器:暂时可以不用处理这个阶段。
5、光栅化阶段:这应该是渲染管线中最复杂的一个阶段,接收三角形三个顶点的输入,输出填充后的三角形内的所有顶点数据,主要是做“插值”。包括顶点插值、纹理坐标插值、法向量插值。
6、片段着色器:光栅化阶段后三角形的顶点数据数量暴增,片段着色器接收一个顶点数据的输入(此时的每个顶点也被称为片段),输出处理完后的一个片段数据。在这个阶段中主要是给每个片段加入颜色、加入光照。最终赋予每个片段以各自的颜色。光照可以参考下Blinn-Phong光照模型,后期拔高的话可以了解下PBR。
二、渲染管线详解
1.顶点输入处理
2.顶点着色器
下面主要讲解几个矩阵的求法:
对于一个顶点坐标 p(x, y, z),我们如何利用一个矩阵对它进行空间变换呢?想必大家都有所了解,聪明的数学家们引入了齐次坐标这个东西(可以方便位移操作)。也就是对于一个三维空间的顶点再加上一个维度w,既是p(x, y, z, w)。这样做的好处就是我们利用矩阵来实现顶点的空间变换啦。看看具体是怎么做的:
(1)缩放
(2)平移
(3)旋转
首先我们讨论一下二维空间的旋转:
1、二维空间绕原点旋转
对于点 v(x, y),绕原点 (0, 0) 逆时针旋转 θ 弧度得到 v'(x', y'):
则有:
x = r * cosφ
y = r * sinφ
x' = r * cos(θ + φ) = r * (cosθ * cosφ - sinθ * sinφ) = x * cosθ - y * sinφ
y' = r * sin(θ + φ) = r * (sinθ * cosφ + sinθ * cosφ) = x * sinθ + y * cosφ
则有:
=
*
2、二维空间绕任意点旋转
绕任意点 (p, q) 旋转 θ 弧度,可以拆分成三步:将这个任意点平移到原点、绕原点旋转、逆操作平移回去。既然涉及到位移,那么就要引入齐次坐标w。
首先对平移到原点:
*
其次绕原点旋转 θ 弧度: