天天看点

软件光栅化渲染器知识总结

简单的CVV裁剪

经过了透视变换,坐标被变换到CVV空间,此时仍然是齐次坐标,我们正常应该是判断在裁剪的立方体内,不过齐次坐标我们也就是直接比较xyz值和w的值即可,DX模式的话,z需要比较0和w。这个是非常重要的,因为我们默认为了方便是把投影平面放到了眼睛前面,但是真的有在投影平面后面的东西,如果不剔除z<0的内容,就会导致这一部分按照不对的透视公式进行计算导致结果错误。而且更重要的一点在于,相机空间z = 0的时候(也就是齐次空间的w = 0)的这种情况,在我们透视除法的时候会有除0的问题。所以要把这个剔除掉。

比如一个齐次空间的顶点,我们可以按照上述方式判断其是否在CVV内:

// 判断点vertex是否在CVV内,在的话返回false
bool SimpleCVVCullCheck(const Vertex& vertex)
{
	float w = vertex.pos.w;
	// x,y,z只要有一个不符合范围就返回true
	if (vertex.pos.x < -w || vertex.pos.x > w)
		return true;
	if (vertex.pos.y < -w || vertex.pos.y > w)
		return true;
	if (vertex.pos.z < 0.0f || vertex.pos.z > w)
		return true;
	return false;
}
           

CVV裁剪个人感觉是一个比较有争议的地方,现代的GPU到底如何去做裁剪,我不敢妄加推测,看了知乎上的讨论,也是分为几个派别。有认为裁剪的,有人为只剔除不裁剪的。不过个人倒比较赞同,重新构建一个三角形对于GPU来说还不如把整个三角形都画了好,毕竟实际运用时,三角形的密度很大,面积很小,都绘制了也要比裁剪可能还省。对于CVV中比较好处理的主要在于我们可以在透视除法前就把完全不可见的三角形直接剔除掉。所以我只实现了最简单的三顶点均不在CVV内剔除的方案

参考 从迷你光栅化软渲染器的实现看渲染流水线

背面剔除

正向背向面剔除可以在NDC中进行,先计算三角形表面法向量,根据法向量和view向量的夹角进行剔除:

enum Face {
	Back,
	Front
};
//面剔除,剔除正向面或者逆向面
bool FaceCull(Face face, const glm::vec4 &v1, const glm::vec4 &v2, const glm::vec4 &v3) {

	glm::vec3 tmp1 = glm::vec3(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z);
	glm::vec3 tmp2 = glm::vec3(v3.x - v1.x, v3.y - v1.y, v3.z - v1.z);

	//叉乘得到法向量
	glm::vec3 normal = glm::normalize(glm::cross(tmp1, tmp2));
	//glm::vec3 view = glm::normalize(glm::vec3(v1.x - camera->Position.x, v1.y - camera->Position.y, v1.z - camera->Position.z));
	//NDC中观察方向指向+z
	glm::vec3 view = glm::vec3(0, 0, 1);
	if (face == Back)
		return glm::dot(normal, view) > 0;
	else
		return glm::dot(normal, view) < 0;
}
           

参考 一篇文章彻底弄懂齐次裁剪

深度测试

ZBuffer

透视插值

三角形面上的正确插值不是线性的,这是由于在投影平面上的相同步长随着三角形面与相机之间的距离增加而在三角形面上产生了更大的步长。图形处理器必须采用非线性插值的方法来计算纹理坐标映射,以避免纹理映射图的扭曲变形。

透视插值的公式:

软件光栅化渲染器知识总结
软件光栅化渲染器知识总结

Phong着色

顶点属性经过插值后,在像素着色器中进行着色

切线空间法线贴图

用三角形的顶点坐标以及纹理坐标可以计算切线向量和副切线向量。然后可以构建可以把切线坐标空间的向量转换到世界坐标空间的矩阵TBN矩阵。TBN矩阵这三个字母分别代表tangent、bitangent和normal向量:这三个相互垂直的向量,它们沿一个表面的法线贴图对齐于:上、右、前。

参考 法线贴图

阴影映射

方向光阴影

第一步,我们需要生成一张深度贴图(Depth Map)。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形。

第二步,判断是否为阴影。先把世界空间顶点位置转换为光空间,然后把光空间片段位置转换为裁切空间的标准化设备坐标,做透视除法后得到[-1,1]范围的NDC坐标,变换到[0,1]后,用x和y采用深度贴图,采样出来的深度closestDepth和片段的当前深度也就是z坐标比较,若 z > closestDepth,则为阴影片段。

阴影失真(Shadow Acne)问题

因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的同一个值中去采样。有些在地板上面,有些在地板下面,这样我们所得到的阴影就有了差异。因为这个,有些片段被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。

可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片段就不会被错误地认为在表面之下了

软件光栅化渲染器知识总结

偏移量计算:

我们有一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直(与光线平行),得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。

悬浮(Peter Panning)问题

使用shadow bias时偏移值过大导致的。使用普通的偏移值通常就能避免peter panning。

阴影锯齿问题

深度贴图有一个固定的分辨率,多个片段对应于一个纹理像素。结果就是多个片段会从深度贴图的同一个深度值进行采样,这几个片段便得到的是同一个阴影,这就会产生锯齿边。

可以使用PCF缓解:一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来。

点光源阴影

算法和定向阴影映射差不多:我们从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。

对于深度贴图,我们需要从一个点光源的所有渲染场景,普通2D深度贴图不能工作;改为使用立方体贴图,立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。

参考 阴影映射 和 点光源阴影

Blinn-Phong反射模型

参考 图形学/OpenGL/3D数学/Unity 第43条。

SSAO

对于铺屏四边形上的每一个片段,我们都会根据周边深度值计算一个遮蔽因子。这个遮蔽因子之后会被用来减少或者抵消片段的环境光照分量。遮蔽因子是通过采集片段周围法向半球型核心(Kernel)的多个深度样本,并和当前片段深度值对比而得到的。高于片段深度值样本的个数就是我们想要的遮蔽因子。

继续阅读