资讯详情

3D软渲染器记录

GitHub地址:https://github.com/Khasehemwy/SoftwareRenderer


文章目录

  • 杂项
    • 行矢量、列矢量和矩阵
    • view矩阵
    • projection矩阵
    • 齐次坐标
    • 法线矩阵
  • 光栅化
    • 线框绘制
    • 线段光栅化算法(直线光栅化)
    • 片段着色器
      • 1) Edge Walk(扫描算法)
      • 2) Edge Equation(边界盒算法)
    • 渲染透明物体
  • 光线追踪
    • 光线跟踪和光栅化
    • 基本步骤
      • 生成相机-像素射线
      • 射线与三角形求交
      • 重心坐标插值计算其他信息
      • 继续生成射线并递归
  • 光照
    • 着色
      • 1) Gouraud Shading(高洛德着色)
        • 定向光(高洛德着色)
        • 点光源实现(Point)
        • 聚光源(类似手电筒)的实现
        • 多光源
      • 2) Phong Shading(冯氏着色)
    • 阴影
      • 软阴影
        • · PCF
        • · VSM
        • · PCSS
        • · DFSS
    • 法线贴图
    • 视差映射(Parallax Mapping)
    • 全局光照
      • 环境光遮蔽(AO)
        • AO算法
      • 间接光照
        • RSM
        • LPV
        • VXGI
      • SSDO
      • SSR
  • 纹理
    • 纹理采样
    • 纹理光照
    • Mipmap
  • 优化
    • 背面剔除
    • 透视修正
    • 水平切割,垂直切割
    • 抗锯齿
    • 双线性滤波 / 三线性滤波 / 各种滤波
      • 双线性滤波
      • 三线性滤波
      • 各向异性过滤
    • 延迟着色(Deferred Shading)
    • 空间划分算法
    • Z-test,Early-z和Pre-z
  • 导入外部模型


杂项

行矢量、列矢量和矩阵

当使用列矢量时,矩阵根据顺序阅读。使用行矢量时,顺序变成(想象转移结果,然后将表达式代入此转移,从列矢量中获得行矢量)

view矩阵

使用相机的look_at()来生成view矩阵时,up矢量写成 你可以生成标准的左右手系,被子mini3d那个项目的 up矢量坑花了很长时间才发现…

长期测试不是标准的左右手系,然后检查行列矢量的使用情况,model矩阵,向量和矩阵乘法,perspective透视生成,屏幕坐标生成,frame_buffer找了很久才发现是世界up矢量的问题

projection矩阵

这里讲得很清楚:https://zhuanlan.zhihu.com/p/74510058

我一直不知道最后的w值是为什么用的。我查了很多资料,终于知道了。w值是储存的原始z值。

将视锥内点 x , y 范围分别映射到[-1,1];z 范围映射到[0,1]。注意,这是w=1点的情况。重量一般乘以z值,最后x,y就在[-w,w],z在[0,w]这里的w是乘z后的w,即z值)。

透视投影后,z与观察坐标相比,值会远离自己。(这两个位置的z值除了远近平面不变)。 如果靠近平面太近,大部分z值会落后,精度低。

齐次坐标

用N N维矢量用1维表示(x,y,z,w)。渲染一般将w设置为1,便于操作。平移必须使用多维矩阵,因此坐标的变化很容易表示齐次坐标。

w透视投影的几何意义在于相机与近平面的距离。

法线矩阵

[参考](https://zhuanlan.zhihu.com/p/72734738#:~:text=有些-,情况-下(如,仅)

当进行MVP顶点坐标变换时,此时法线不能跟随乘坐MVP矩阵变换。法线和顶点坐标一起变换,

在这个时候,需要一种方法法线仍然垂直于顶点。我们用法线矩阵来表示法线×法线矩阵 之后可以和顶点一起变换,垂直于顶点。

推导后可以得出法线矩阵: = (顶点转换的逆矩阵)转换矩阵。如果顶点转换为世界坐标(Model矩阵),则

可以使用伴随矩阵的转移作为法线矩阵。(方阵的逆阵是方阵的伴随矩阵,除以方阵的行列。也就是说,伴随矩阵和逆矩阵只有一个系数(即只影响转换后法线的长度),而伴随矩阵总是存在,所以我们可以使用伴随矩阵的转移作为法线转换矩阵。请注意,通过转换获得的新法线不一定是单位长度,需要集成。



光栅化

线框绘制

画线段。

线段光栅化算法(直线光栅化)

画直线的光栅化算法

因为在搜索三角形光栅化相关算法时,可以进一步了解直线光栅化。

数值微分有三种基本算法:DDA(Digital Differential Analyzer)算法,中点画线算法,Bresenham算法。 DDA很简单,直线方程稍微推导一下,然后Bresenham是DDA结合中点画线的优点,也采用了我的软渲染Bresenham算法光栅化直线。

片段着色器

相关介绍

参考: 三角形光栅化 光栅填充三角形算法 How to determine if a point is in a 2D triangle? 三角形光栅化时遇到的坑

一般有两类:。 为了好理解,可以把Edge Wak叫扫描线算法(SCANLINE),Edge Equation叫边界盒(BOUNDINGBOX)算法(我自己的叫法…)。

1) Edge Walk(扫描线算法)

思想是画三角形的左右两条边,当y值相同时就水平画一条线来填充三角形。计算比较复杂,适合cpu,一般比较快。

(有缺陷 --> 已解决)

拼接处有毛刺和拼接不齐问题 是当y的小数部分舍去的时候,如果斜率特别小,y变化一点点就会导致x变化特别大,然后就导致了x方向突增或突减。

解决:将所有逻辑改为向上取整,即可解决此问题。因为向上取整会保证所有坐标点都会落在原始三角形内,而向下取整会导致某些不在三角形内部。

问题是最开始取y值的时候,y值的小数点部分舍去会导致x出现偏差,需要根据y值的变化去修正x。

解决: xleft = x0 +(ceil(y0) - y0) * xleft_step; xright = x1 + (ceil(y0) - y0) * xright_step;

和线段的Bresenham算法差不多,但是是同时画两条线,三角形左边和右边,假定左边先画,且从上到下画,当左边的y增加1时,要暂停等右边的y也到这里,然后左y=右y时,在该y坐标画一条x1到x2的直线。

目前试验该算法不会有拼接不齐问题,毛刺现象极少发生,可暂时忽略。

用Bresenham算法来绘制三角形不太好进行颜色的线性插值。

2) Edge Equation(边界盒算法)

这个的思想是先计算出三角形的包围盒(把三角形包住的矩形),然后遍历包围盒中的每个像素点,判断是否在三角形内,如果在内部就着色。

边界盒着色比较精准,但是一般比扫描线算法慢。

Edge Equation的插值可以用三角形重心插值。

透明物体渲染

在z-buffer引入前,光栅化通常都是用画家算法,即对每个物体排序,再从后往前绘制。引入z-buffer后,不透明物体渲染可以顺利解决了,但是半透明物体不行。

实现 的技术通常为 ,混合的最终效果和次序有关(混合时,需要把前面步骤计算得到的颜色当作整体,再乘混合因子,显然次序改变后结果不同)。所以渲染透明物体的做法之一就是对透明物体从后往前绘制,这是次序有关的半透明混合。

对透明物体排序开销较大,而且物体粒度太大,不易处理。所以需要

次序无关的半透明处理常用算法:

每次找出从视点出发,没处理过的最前面一层的半透明图层,然后记录,并标记为处理过,最终到达预设的N层或者剥离完为止。最后把每层的颜色混合起来。 Depth Peeling 是一种很慢,非常费显存空间(空间分配可确定),但对硬件没什么高要求的 OIT 方法。

为每个像素开一个链表,每次遇到这个像素有半透明片段就把该半透明相关信息插入该链表。最后将链表中的内容排序,再混合。 速度比 Depth Peeling 快了很多,显存空间也比 Depth Peeling 更加节省,但节点纹理具体需要多大无法事先做准确的预估,因此显存的具体消耗不可控。

,它将经典混合公式进行了修改,引入了一个能见度函数,修改后的混合公式最终结果与次序无关。完美的做法是遍历链表来建立能见度函数,但这样就和原本的逐像素链表一样了。一个很好的思路是将能见度函数近似的表示为一个单调递减的定长数组,这样就做到了优化。当然,简化了混合计算复杂度,但也因此丢失了混合精度。

将alpha值换成mask来决定像素是否写入颜色缓冲区。例如:alpha为0.5时,mask值为0.5也就是说绘制时有一般像素会被绘制另一半不会绘制,alpha越小被绘制的像素越少。缺点很明显,像素感较强,不过有时也可满足特定目标。

对于某个位置的像素点,如果所有的半透明物件是相同的颜色,那么渲染的结果与它们的渲染顺序无关。那么,。对于这种情况,我们使用各个颜色的不透明度作为权重来计算出它们的平均值。 此算法的优点很明显,效率高,速度快,只需要对物体进行一次的渲染,然后加上一次全屏的后处理。但是缺点也是同样的明显,透明结果只是一个近似值,而不是确切的正确结果。

参考:https://zhuanlan.zhihu.com/p/353940259 https://www.zhihu.com/question/382932468/answer/1111595945 Screen-Door transparency(纱门透明度)



光线追踪

效果:

光栅化(无阴影)

光追(Whitted-Style)

光追(Whitted-Style),添加全局光照Trick(右侧方块有部分蓝色反光)

光追(PBR),4深度,4采样点

光追(PBR),16深度,2048采样点

参考: smallpt: Global Illumination in 99 lines of C++ GAMES101 (光线追踪教程) Ray Tracing: Rendering a Triangle (计算射线与三角形相交) Ray-Tracing: Generating Camera Rays (生成相机到屏幕射线) Invert 4x4 matrix (计算4x4矩阵的逆矩阵) Random float number generation (生成[l,r)的随机数)

光线追踪与光栅化

先回顾光栅化,我们是把三角形的坐标经过,最终确定三角形的某部分对应屏幕的某个像素,然后计算这个部分的颜色(比如光照等),最后得到这个像素的颜色值。

是把光栅化过程反过来。我们把坐标统一到世界坐标,确定好观察位置(摄像机位置),然后把屏幕上每个像素的坐标变换到世界坐标(后面会介绍如何变换)。现在,得到相机位置和像素位置,我们可以求出以相机为起点,相机到像素的向量为方向的一条射线。这条射线其实就是光线传播的逆过程。

基本步骤

这里是最基本的步骤,不涉及加速结构、光照计算方式等探讨。

1.遍历屏幕像素,为每个像素,并进行后续计算。 2.储存所有物体的三角形片元,每次射线,以计算交点。 3.计算出交点后,可以采用,计算出该点的其他信息。 4.采用递归,以交点为起点,在场景中追踪,最后确定出起点像素的颜色。当然应该设定最大递归深度。

生成相机-像素射线

Ray-Tracing: Generating Camera Rays (生成相机到屏幕射线)

仿照MVP矩阵的思路,比较容易得出像素(其实也没那么容易,可以看上面的参考)。具体是 像素真实坐标 -> NDC(Normalized Device Coordinates)坐标 -> 透视裁剪坐标 -> 观察坐标。

得出像素在观察空间的坐标后,只需要把该坐标乘上MVP矩阵里,就可以把坐标从观察空间转变为世界空间了。相机的世界坐标是已定义的,可以直接得出射线。 参考:Invert 4x4 matrix (计算4x4矩阵的逆矩阵)

当然,有时射线的生成需要一些(真实世界有无数光线,不能模拟无数光线,只能模拟一定数量的随机光线了)。我们一般在像素真实坐标时,引入随机偏移(遍历像素时x递增1代表移动1个像素,此时让x偏移[0,1)就等于在这个像素中进行偏移)。 参考:Random float number generation (生成[l,r)的随机数)

射线与三角形求交

Ray Tracing: Rendering a Triangle (计算射线与三角形相交)

首先,肯定是

生成射线后,遍历所有的三角形片元与该射线求交,。射线与三角形求交有多种方法,这里使用Möller-Trumbore algorithm

注意得出参数t值后,需要判断 t>0.0f ,以

若没有任何在射线前方的交点,退出递归。

重心坐标插值计算其他信息

这部分和Phong Shading一样。需要插值计算交点的颜色、法线等信息。

当然,对于不同的着色方法,部分信息用插值计算会出现不太正确的结果。比如法线,我们有时应该用面法线(PBR中),而不是顶点的插值法线。

继续生成射线并递归

现实中的光线会弹射无限次,我们在光线追踪中,也需要不断弹射射线。

弹射的射线就是。方向向量的确定随光线追踪方式不同而改变(比如镜面是完全反射,漫反射是随机方向,折射是和折射率有关,等等)。

确定一个最大递归深度,达到深度后一般会返回黑色({0,0,0})。

递归结束后颜色如何累加,也随具体实现方式不同而变化。



光照

着色

1) Gouraud Shading(高洛德着色)

(无光照的高洛德着色↑)

高洛德着色基本就是算出各个顶点的颜色,然后光栅化时按顶点颜色来插值。

在光栅化扫描线经典算法的基础上完善(Gouraud Shading) (用BresenhamAlgorithm光栅化来完善高氏着色很不好写,很繁琐)。

注意颜色插值在经典算法里也同样有拼接不齐之类的问题,需要修正。而且注意能用原始float数据就不要换成int截取的数据,容易出问题(被坑了很久)。

目前发现颜色有需要透视修正的问题。(已解决)

定向光实现(高洛德着色)

学了下OpenGL的光照部分,对光照有了初步理解,开始写一个最简单的平行光照+环境光+漫反射。

参考: LearnOpenGL:基础光照

(添加镜面光照)

(前方白色矩形为光源。对光源使用了另外的着色器,以保证光源为常亮白色)

光照主要是光源的,光源的颜色一般就包含在环境光和漫反射光部分了。为啥一个光源有这些定义?因为光对的影响主要就是这些,为了方便光照处理时和材质的相关计算,就这么定义了。

平行光源就没有光衰减什么的了,所以很简单,先实现它。

目前也没有定义材质,是从顶点的颜色来处理的。

因为光照需要z轴信息,所以当顶点映射到世界或者观察坐标再处理光照均可,这里选择(更直观)。处理时按照相关公式对顶点颜色进行更改即可。

因为是高洛德(Gouraud)着色,所以是对顶点进行光照处理后,再线性插值。高洛德(Gouraud)着色在镜面光照部分的处理明显不如冯氏(Phong)着色。

环境光实现如下:

		color_t ambient1 = light->ambient * v1.color;
		color_t ambient2 = light->ambient * v2.color;
		color_t ambient3 = light->ambient * v3.color;
		//v1,v2,v3为三角形的三个顶点

漫反射稍微复杂些:(漫反射光照计算相关资料可以去刚刚的参考看)

		// 使用兰伯特余弦定律(Lambert' cosine law)计算漫反射
		vector_t norm = vector_normalize(v_normal);	//顶点所在平面的法向量,先给单位化一下
		vector_t light_dir1, light_dir2, light_dir3;//从平面指向光
		float diff;
		
		light_dir1 = vector_normalize(-light->direction);//平行光
		light_dir3 = light_dir2 = light_dir1;
		
		diff = max(vector_dot(norm, light_dir1), 0.0f);
		color_t diffuse1 = light->diffuse * diff * v1_tmp.color;

		diff = max(vector_dot(norm, light_dir2), 0.0f);
		color_t diffuse2 = light->diffuse * diff * v2_tmp.color;

		diff = max(vector_dot(norm, light_dir3), 0.0f);
		color_t diffuse3 = light->diffuse * diff * v3_tmp.color;

镜面反射光照:

		vector_t reflect_dir1, reflect_dir2, reflect_dir3;
		vector_t view_dir1, view_dir2, view_dir3;
		reflect_dir1 = vector_normalize(vector_reflect(-light_dir1, norm));//reflect计算反射光
		reflect_dir2 = vector_normalize(vector_reflect(-light_dir2, norm));
		reflect_dir3 = vector_normalize(vector_reflect(-light_dir3, norm));
		view_dir1 = vector_normalize(camera->pos - p1);
		view_dir2 = vector_normalize(camera->pos - p2);
		view_dir3 = vector_normalize(camera->pos - p3);
		float shininess = 32.0f;

		float spec = pow(max(vector_dot(view_dir1, reflect_dir1), 0.0), shininess);
		color_t specular1 = light->specular * spec * v1_tmp.color;

		spec = pow(max(vector_dot(view_dir2, reflect_dir2), 0.0), shininess);
		color_t specular2 = light->specular * spec * v2_tmp.color;

		spec = pow(max(vector_dot(view_dir3, reflect_dir3), 0.0), shininess);
		color_t specular3 = light->specular * spec * v3_tmp.color;

添加光照处理后透视修正会失效,尚不清楚原因。

注意这些光照处理都是在进行的,观察坐标也可以光照处理,但相关方程需要变化。

点光源实现(Point)

点光源和定向光源主要的区别:1.光的方向不是平行的;2.光会衰减。

漫反射的光照修改:(当然对应的镜面光照要使用的 light_dir 也更改了)

	light_dir1 = vector_normalize(light->pos - p1);
	light_dir2 = vector_normalize(light->pos - p2);
	light_dir3 = vector_normalize(light->pos - p3);

衰减计算:(用衰减公式)

	//这里只写对一个顶点的处理,其他两个顶点类似
	float distance1 = vector_length(light->pos - p1);
	float attenuation1 = 1.0 / (light->constant + light->linear * distance1 +
		light->quadratic * (distance1 * distance1));

	v1_tmp.color = (ambient1 + diffuse1 + specular1) * attenuation1;

聚光源实现(类似手电筒)

聚光源原理也不阐述了,之前的链接里讲了。

聚光源的衰减可以用点光源的衰减,然后和点光源的区别:有一个,在切光角外的部分几乎没有光。

所以我们需要 点到光源的方向向量和光源方向向量的夹角 的相关信息,来判断这个点是否在切光角内。

然后再加个,来平滑边缘。

		float epsilon = light->cut_off - light->outer_cut_off;
		float theta1, theta2, theta3;
		float intensity1, intensity2, intensity3;
		vector_t light_direction = vector_normalize(-light->direction);

		theta1 = vector_dot(light_dir1, light_direction);
		intensity1 = CMID((theta1 - light->outer_cut_off) / epsilon, 0.0, 1.0);
		diffuse1 *= intensity1; specular1 *= intensity1;
		//另外两个顶点类似

目前发现在距离一个平面特别近时,聚光会反而变暗消失。猜测是因为高洛德着色对顶点处理,而距离平面特别近时,顶点和摄像机的夹角特别大,在切光角外面去了,所以变暗。

多光源

不是全局光照,没有考虑反射折射那些,就是单纯的多个光源效果叠加。颜色相加,比较简单。

(图中右上角大的定向光,下方点光源,摄像机位置有朝前的聚光源)

2) Phong Shading(冯氏着色)

重心坐标计算 表面法向量插值 重心坐标插值 重心坐标插值的透视修正

冯氏着色比高洛德着色真实,但效率低一些。

高洛德着色:对三角形顶点进行光照计算,算出三角形顶点的颜色后,在光栅化时插值计算颜色。 冯氏着色:对三角形的世界坐标、法线和每个像素点的颜色进行插值,然后再用每个像素的信息去计算光照。显然,每个像素都进行光照计算,会慢很多。

从阴影这部分可以得到世界坐标插值的结果(因为阴影需要用到世界坐标),拿到世界坐标后,可以用,用重心坐标的系数得出每个像素法线和颜色的插值结果。这里要注意透视修正。

坑点比较多,可以详细看代码:

	//这里最初所有数据都是进行了透视修正的,所以后面会看到 *wi 这个计算
	vertex_t v;
	v.pos = { 
         world_xi , world_yi , world_zi , 1 };//这里不乘wi,因为后面会乘wi
	barycentric_t bary = Get_Barycentric(v.pos, extra_data.world_pos.p1, extra_data.world_pos.p2, extra_data.world_pos.p3);//计算重心坐标系数
	
	v.color = top.color * wi * bary.w1 + left.color * wi * bary.w2 + right.color * wi * bary.w3;
	v.normal = top.normal * wi * bary.w1 + left.normal * wi * bary.w2 + right.normal * wi * bary.w3;
	v.pos = v.pos * wi;//转换回世界坐标,这样光照计算才是正确的
	Phong_Shading(v);
	
	color_use = v.color;

左: 高洛德着色, 右: 冯氏着色 (GIF左侧是帧率,很明显冯氏着色慢一些)

阴影

(下图是高洛德着色下的实现,因为高洛德不好处理每个片元,所以简单地让颜色*=0.5来变暗,不够真实) (下图是基于冯氏着色实现的,真实很多)

软件渲染时期,是计算顶点经过光源投影后的位置(也就是连接顶点和光源,生成一条直线,再看这条直线和哪些平面相交),顶点数选取可自己确定,顶点越多,阴影形状越真实。

硬件渲染不能用这么麻烦的计算了,所用技术有

下面介绍阴影贴图(Shadow Mapping)的做法(效果图的实现方法也是阴影贴图)。

1.以光源为视角渲染场景,记录里的值,把这个值存到一个贴图里,这个贴图就是阴影贴图。

2.以正常视角渲染场景,但此时需要知道每个点对应的光源空间中的坐标,将点变换到光空间后,记录这个点此时的z值,再对阴影贴图进行采样得到,如果Current Depth > Shadow Depth,说明该点在阴影中,此时标记一下,再在正常视角中处理阴影。

步骤2这里有一些不太好处理的地方:

  • 正常视角渲染时,怎么得到像素在光空间中的坐标? 一种容易想到的是直接把光栅化时的坐标乘上正常渲染的观察矩阵和透视矩阵的逆矩阵,变回世界坐标,再乘光源的变化矩阵。但是逆矩阵求解其实是很慢而且不够精确的。我们这样解决:顶点变化到世界坐标以后,记录下来,光栅化时再对世界坐标插值计算新的{x,y,z},最后将得到的坐标变化到光源空间。注意,插值计算世界坐标时也要透视修正。
  • 读取ShadowMap时,应该从插值得到的世界坐标变化到光源坐标系下的屏幕空间,再在这个屏幕空间中取对应点的ShadowMap值,这样才能得到正确结果。

若阴影贴图精度不够,会出现 等问题。

阴影贴图的一个不在阴影中的采样点可能对应了好几个像素,在计算这些像素的时候,可能有的小于贴图的深度,有的大于,于是就会出现交错阴影的现象,本来应该是都被点亮的。可以采用增加 或者 生成阴影贴图时采用物体背面计算深度()来解决。

该被点亮的地方被计算得大于贴图的深度值了,所以可以把计算出的Current Depth整体减小(即减去一个偏移量),这样可以避免部分失真。当偏移量不够时,仍有失真。加上偏移量后,会出现 问题。

原本在遮挡物与阴影接触的地方,深度差值就很小,加上偏移量后可能在这些边缘应该处于阴影的地方,被点亮了,看起来就像阴影悬浮。

比加偏移量好的地方是不会出现悬浮。生成阴影贴图的时候采用背面的深度值,这样在正常渲染的时候,正面的深度值肯定小于背面,所以不会出现某些应该被点亮的地方却计算出在阴影中的情况了。

就是阴影的抗锯齿滤波,和一般贴图在边缘的抗锯齿差不多。PCF时,每次阴影贴图的采样是对周围几个点采样,而不是只采样当前点,最后再平均一下可见性。

软阴影

参考:GAMES202 实时渲染中的软阴影技术

现实中的光源都是有体积的,不是完美的点光源,所以会有部分阴影没有那么黑。具体现象就是从阴影到光亮部分有一个缓慢过渡。

软阴影的生成中心思路就是,再来确定该像素没被照亮的比例。

PCF(Percentage-Closer Filter)、CSM(Convolution Shadow Maps)、VSM(Variance Shadow Mapping)、ESM(Exponential Shadow Mapping)、MSM(Moment Shadow Mapping)、PCSS(Percentage-Closer Soft Shadows)、DFSS(Distance Field Soft Shadows)。

PCF、CSM、VSM、ESM、MSM都是生成一致性软阴影的,PCSS解决的是不同的问题(生成不一致的软阴影),一般和前面的算法结合使用。 DFSS是比较新的技术,不使用Shadow Mapping。

· PCF

显然,PCF会对一个像素取ShadowMap上周围的点来确定遮盖度,这样就可以生成软阴影。缺点也很明显,需要多次采样,效率低。

· VSM

方差阴影贴图。应用了正态分布、切比雪夫不等式等技巧。

回想中心实现思想,其实我们只需要知道。比如取了周围9×9的范围,有30%处于阴影中,那么我们就该应用30%的阴影效果(其实和PCF一样)。

PCF多次采样很慢,VSM就是解决多次采样慢的问题。VSM通过一些公式推导,用一个等式就可近似算出处于阴影中的比例。

VSM是把周围处于阴影中的分布近似为一个正态分布(或高斯分布)。当然这种近似是不完全正确的。当作正态分布以后,我们只需要知道这个分布的,再利用切比雪夫不等式来近似,就可以直接得出处于阴影的比例。

可以用MipMap或者SAT(二维前缀和实现),快速求出某一区域的总和。均值可以从总和得到。MipMap不如SAT精确。

方差Var(X)=E(X2)-E2(X)。E期望就是均值。所以,只需要在生成阴影贴图时,同时储存深度值的平方。平方值一般放在RGBA的G通道上(深度放在R通道)。

求出了均值和方差,再根据切比雪夫不等式进行近似,就可以得到阴影占比了。

。近似认为周围是正态分布,MipMap的均值近似,对切比雪夫不等式近似。这些近似会导致一些问题。比如若周围不是正态分布,就会出现漏光现象。

· PCSS

Percentage-Closer Soft Shadows。软阴影在边界上软的程度是不一样的(越靠近边缘越软),PCSS就是解决这个问题的。

不使用PCSS时,我们对一个像素周围的采样大小是固定的,这就会导致软阴影是一致的。PCSS可以控制

首先,当光源有体积时,才会出现软阴影。有遮挡物,才会有阴影。

1.确定遮挡物的平均深度。2.确定采样范围。3.以采样范围来生成软阴影(PCF、VSM等)。

1.确定遮挡物平均深度。对阴影贴图的一定范围采样即可,这里也可以用一些近似来避免多次采样。

2.确定采样范围。用了相似三角形的思想,如下图。

3.生成软阴影。和PCF之类的步骤一样,只不过采样范围变了。

· DFSS

参考:GAMES202-P5

Distance Field Soft Shadows,距离场软阴影。比较新的技术,相对于前面的算法,思路是比较独立的。使用了Signed Distance Functions来生成距离场,并根据距离场来生成不一致的软阴影。

该技术没有使用Shadow Mapping,而是用距离场来取代。每次渲染前将距离场预先生成好(距离场只需要在有物体运动时或非刚体形变时才需要完全重新生成),然后根据储存的距离场采样计算出非一致软阴影。

距离场需要三维的,所以所需的内存比ShadowMap大很多。但是距离场在某些情况下不用每帧都重新生成,所以比ShadowMapping更快。

对比PCSS,DFSS速度更快、质量更高。但需要预计算、需要大储存空间、和PCSS一样会有一些不精确的地方。


法线贴图

参考:LearnOpenGL

对贴图应用光照最直接的是把整个贴图当作平面,但是如果贴图上有一些坑坑洼洼的地方,这时光照的效果就不够真实。法线贴图就是和贴图附加在一起,标明每个纹素对应的法线。有法线了,光照效果会真实很多。

法线贴图的法线方向一般都是相对于贴图的坐标的,假定贴图正对视线,以世界坐标为基准,那么贴图的坐标就是z轴朝外(世界坐标的z轴),法线也z轴朝外。但是当贴图放平时,贴图的法线变成y轴朝上了,但法线贴图还是z轴朝外,结果显然不正确。

我们把法线贴图总是基于贴图的正z轴方向的空间定义为切线空间。为了解决上述问题,我们可以想到,应定义一个转换矩阵,可以让法线贴图从切线空间转换到世界坐标时与贴图能正确对应,或者用这个矩阵把需要的信息(光源方向、视线方向)变化到切线空间中去。这个矩阵就叫(tangent、bitangent和normal)。

可以把切线坐标的向量转换到世界坐标(当然TBN得提前左乘一个Model矩阵)。或者求TBN的逆矩阵,可以把世界坐标转换到切线坐标。

TBN矩阵求解推导

TBN矩阵的求解方法如下:

//假设平面使用下面的向量建立起来(1、2、3和1、3、4,它们是两个三角形)
// positions
glm::vec3 pos1(-1.0,  1.0, 0.0);
glm::vec3 pos2(-1.0, -1.0, 0.0);
glm::vec3 pos3(1.0, -1.0, 0.0);
glm::vec3 pos4(1.0, 1.0, 0.0);
// texture coordinates
glm::vec2 uv1(0.0, 1.0);
glm::vec2 uv2(0.0, 0.0);
glm::vec2 uv3(1.0, 0.0);
glm::vec2 uv4(1.0, 1.0);
// 贴图当前在世界坐标的法线方向
glm::vec3 nm(0.0, 0.0, 1.0);

//第一个三角形的TB向量计算(点1、2、3), 第二个类似
glm::vec3 edge1 = pos2 - pos1;
glm::vec3 edge2 = pos3 - pos1;
glm::vec2 deltaUV1 = uv2 - uv1;
glm::vec2 deltaUV2 = uv3 - uv1;

GLfloat f = 1.0f / (deltaUV1.x * deltaUV2.y - deltaUV2.x * deltaUV1.y);

tangent1.x = f * (deltaUV2.y * edge1.x - deltaUV1.y * edge2.x);
tangent1.y = f * (deltaUV2.y * edge1.y - deltaUV1.y * edge2.y);
tangent1.z = f * (deltaUV2.y * edge1.z - deltaUV1.y * edge2.z);
tangent1 = glm::normalize(tangent1);

bitangent1.x = f * (-deltaUV2.x * edge1.x + deltaUV1.x * edge2.x);
bitangent1.y = f * (-deltaUV2.x * edge1.y + deltaUV1.x * edge2.y);
bitangent1.z = f * (-deltaUV2.x * edge1.z + deltaUV1.x * edge2.z);
bitangent1 = glm::normalize(bitangent1);//实际上这里可以直接用T向量和N向量叉乘得到

有了TBN矩阵,仿照MVP矩阵的使用,很容易就能正确应用法线贴图了。

视差映射(Parallax Mapping)

法线贴图可以纠正光照结果,但凹凸不平的高度不止影响光照,也。对纹理的具体体现就是采样时坐标不应该直接对应,应该有一点点偏移。

我们要确定这个采样偏移量,所用的方法就是(或者叫视差贴图,但是概念上其实计算采样偏移量只需要一个高度贴图,后面的计算应该叫视差映射)。

标签: sh8c15连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台