学习笔记23

这里先谈前向渲染中的雾。

我们做之前,做好把HDR关了,然后forward。

关于线性雾的原理。就是通过距离来获得一个factor,然后把这个factor作为插值系数来计算物体被雾朦胧化之后的结果。

还有一个就是指数雾,这个计算公式是一个指数:

还有一个与其类似的:

雾打开的地方。

现在雾气的种类基本上就是这三种,然后我们现在自己的shader放在有雾的环境下,还不能出现任何的朦胧效果,所以下面的任务就是让我们的shader来支持这一点。

首先要加一个这个东西,他会搞出来三个关键词,就是对应于刚刚我们说过的三种雾。

然后我们在面板中做不同的指定,可以激活不同的关键词,生成相应的变体。

然后新建一个函数,在这里我们计算出来距离,因为雾的核心变量就是距离,摄像机和fragment之间的距离。

然后就是直接根据距离来进行一个插值,这个都帮我们写好了,UNITY_CALC_FOG_FACTOR_RAW这个就是根据我们的关键词的激活情况,

在外面面板中的选项,来选用特定的计算方式。

这里有两个小细节,就是这里的计算方式有可能是linear的,所以我们保险需要给他做一个裁剪到0到1.

然后就是插值时,不需要对a进行插值。

现在得到的效果和标准着色器还是有一些差距的,

这是因为Unity处于性能的考虑,算法上做了一些调整。

核心点就在于,它不是通过距离来计算最终雾,而是通过裁剪空间下的深度值。

这里的好处就是,不用计算距离的平方根了,效率更高。

但是,其实他这个就不对了,我们拿距离来计算其实是更加合理一些的,更加合乎真实情况。他这个尤其是side-scroll侧面

周围转动的时候,就会导致同一个点在转动不同角时看上去的雾的浓度不一样。

但是这个在多数场景下效果并不明显,可以接受。

操作上,这里定义了一个宏来看看是否采用这种情况,另外我们需要把计算好的深度传递给fragment,

这里只需要把之前的worldPos改成四维的就好了,因为一个深度,用一个维度就够了,worldPos既然是必须要有的,就凑着和它一起传过去。

最后鉴于一些裁剪空间可能配置各不相同的情况,这里Unity有搞了一个官方的宏出来。

这里关于这两者的选择,这里是搞了一个都支持,但是这里并没有像之前的renderingMode那些在面板上去开放参数来让用户调节选择。

说实话这点效果差距本来就不大,所以不配,使用一个宏来判断就够了。

这里就给shader加一个雾开关的代码,对于最外面虽然有一个雾的开关,但是那个一关,整个场景所有物体都没有雾了。

这里是单独给当前材质的物体设置的一个开关。

最后这个多光源下的雾会导致多次叠加之后,如果是比较亮的雾颜色,就会导致曝光。

所以解决就是黑色的雾。

也就是在多灯光的情况下,我们的每一个pass,他都会走一遍雾的计算。如果雾的颜色本身比较白,那么如果距离稍微大一些

这就会导致每次lerp之后的结果是一个偏向白色的结果。

偏向白色之后,多个pass累计之后就出现了曝光的现象。

所以按照上边所说,对base pass使用正常的雾气的物理计算,其他的pass雾都直接认为是黑色的,这样就不会曝光了。

当我们考虑到把渲染路径改为延迟渲染的时候,这里修改为他是没有任何雾效果的,而且不只是我们的shader没有。

连standard shader都没有,这是因为延迟渲染的雾效果,需要在光照pass执行完成之后,算是一个类似后处理的效果。

这里需要注意,就是我们摄像机虽然选择的是延迟渲染,但是如果物体的mode改成了透明,那么物体就不会去走延迟渲染的pass

因为透明在延迟渲染中也是需要单独实现的,就是和前向渲染中一样的实现。

这里改成了透明之后,少了两个球,其实这个是渲染顺序的问题,先渲染了最后的俩球,然后渲染的地板,由于没有深度写入

所以这里效果上就直接覆盖了。

这个渲染的pass,是有一些不同的,其实这也是延迟渲染和前向渲染的一个不同点。

就是延迟渲染中,我们始终都是使用一个shading 算法,来对整个场景的所有物体进行着色计算。这是它的计算方式决定的,就得这么玩。

那么我们现在要添加一个雾效果,首先他得是一个单独的pass,因为他需要在光照计算完成之后,然后再对每一个fragment进行雾效果处理。

这种单独的pass,也是直接应用于整个场景的,所以他的使用方式和我们之前前向渲染的pass也有些不同

总的思路就是在摄像机上加一个component,也就是脚本,然后用脚本来控制实现这个。

这里就介绍了这些component是如何起作用的,就是渲染完场景之后,会检测这些component和他们的方法,并且调用这些方法。

如果有多个component的话,就挨个调用。

这里基本上说的都很清楚了。

我们要想在rendered scene结果上在做处理,就可以用这个OnRenderImga。

如果说,按照下面的只是写一个空的OnRenderImage函数,那么会导致整个输出的结果就是黑的,因为他调用了这个,

就意味着把destination作为最终的输出,而我们没有往里面写任何的东西,所以就是黑的。

当然,这里在scene和game视图下都是正常的,不是黑的,但是一旦运行就会变成纯黑的。

而如果我们加上了[ExecuteInEditMode]

在Game视图下就可以显示纯黑的了,其实这个EditMode应该指的就是非运行状态下,我们在scene里面各种操作编辑的状态吧。

这里就实现了后处理效果,但是这里处理只是把原图复制原样输出了一下。

我们想要控制这个后处理的具体操作,就要自己写一个shader

要想shader起作用,我们需要创建一个材质。

所以整体代码结构已经出来了,就是在类里面开放变量shader,然后我们在面板中可以对他进行赋值。

得到了赋值的shader,然后创建材质变量。有了材质,我们就可以利用Blit函数来对source图像进行处理并输出到dest/

这里的shader,我们就可以重新写在一个文件里面,因为shader处理的对象是一个rendered之后的结果,所以不需要cull,zwrite灯

其实这里虽然已经render完成了,但是他还是保存着那些深度,UV,法线等等的信息。所以你依旧可以想象他是在3D场景下的一个个fragment

所以下面的写法就很好理解了:

这里就是把原图复制了一遍。

Blit函数的作用就是,采样第一个,然后根据第三个参数加工采样结果(如果有的话),然后放在第二个参数中。

根据之前说的,我们仍然是有存储的那些深度等等的信息,所以这里可以采样深度,然后根据深度搞出来基于深度的雾

然后这些采用都用宏来实现比较好。

这里他又进行了一个把depth线性化的操作。

具体的执行操作是是使用了一个ZBufferParams的东西。里面是存储了很多固定的值,什么far/near之类的,具体如上

作用肯定是便于我们直接用它可以做一些数值的变换了。也就是这里怎么变换到linear的,具体推导先不管

先思考一下这个线性坐标的问题。

思考之前先来回顾一下之前在前向渲染中我们使用的深度坐标,那个是函数的裁剪空间的坐标,也就是那个和MVP矩阵相乘之后的坐标。

这个坐标有一个重点,就是他的xyz值并没有除以w,也就是它还不是NDC坐标。

这就有意思了,其实我们仔细看一下公式推导中的xyz项,其实他们都是线性的。

怎么说是线性的呢?首先MV一定是线性的结果,P其实也是,我们看公式,公式推导的变换之后的xyz项,

下面是一个观察空间坐标,乘以P矩阵,变换到裁剪空间的坐标的

变换后的xyz项,里面都包含了没有变换的xyz,而我们仔细看,他们都是和一个固定的系数相乘,或者相加减。

其实这个变换就是线性的,什么是线性的呢?其实就是所有的Z或者x或者y都以相同的比例来缩放,都以相同的距离来平移。

这样得到的结果就是线性的。

但是如果考虑上边的结果除以w,也就是这里的-z之后,一切都变了,他们的xy的缩放的系数,都变成了和自己的z相关的,

也就是说,不同的坐标,xy缩放的比例不一样,那么就会产生非线性的变换,而z这里就直接变成了1/z的线性变换,那也不是关于Z的

线性变换。

所以结论:裁剪空间的值是线性的,NDC空间的值不是线性的。

所以之前我们使用的深度值,是裁剪空间的深度值,是一个线性的深度值,然后拿他作为雾的求解变量。

回到题目来,这里线性化之后,还没有完,这里还需要一个缩放。

经过这个缩放之后,就获得了viewDistance,然后剩下的就和之前类似了。

这里出效果之后,还是有一些小的问题:

这里有点不太能理解,为什么在透明物体上化雾,就是错的,难道不就应该画上去吗?

我们的眼睛和透明物体之间的雾成分,必然会对透明物体射入人眼的光造成一个影响,感觉把雾画上去没啥问题啊。

但是这里学会这个attribute为主。

还有一个就是这里的HDR需要开启一下,

最后这里还有一点小的问题,需要减去近裁剪平面的距离,但是这个减去也不会exact match,但是unity就这么减的,我们也这么干得了/

接下来这个,就是我们根据距离来计算雾了。那么我们就要获取fragment和摄像机之间的距离。

首先这个距离,我们在前向渲染的时候,取的是世界空间下的距离。

所以这里我们也要获取世界空间下的距离,但是这一点并不是那么容易,这里采用了一种绝妙的方式,来reconstruct世界空间。

整体的思路:因为我们拥有每一个像素的深度信息。

从这里的一段可以明显的看出来,在这个后处理的pass中,我们执行的有点类似一个quad的渲染。

主要是因为,我们从C#那边,把一个frustum或者pyramid的四条棱的向量传递了进来,然后关于每一个像素是如何获得这个ray向量的,

他们是通过插值获得的。

这里的vertex程序就是进来了,底边的四个顶点。

这里一句,就是最核心的ray 相交的了。

因为我们插值的来的向量,一定是从摄像机的位置为起点,远平面的fragment处为终点的一个向量,那么现在乘上depth,他是一个从0到1的范围。

也就是对这个向量做一个scale。

如果该fragment的位置本来是在pyramid的中间,那么他的depth是1/2,那么向量就被缩减到1/2长度,也就是从摄像机到它未投影时位置的距离。

也就是世界空间中的距离,而且更牛逼的是,如果这里把得到的向量,去加上摄像机的世界坐标,得到的其实就是fragment在世界空间中的位置。

注意这里把frustum直接当作了pyramid来处理,其实这个完全没有问题。这里只是忽略了近平面,而近平面就是用来做范围的裁剪的,

其实对这里没有任何的影响,这里是已经裁剪完了的fragment,投影了的fragment,倒退他们世界的位置。

这里最为核心的是frustum的四个棱向量的求解。这四个必须是世界空间下的,当然这里由于我们只需要长度,其实观察空间下的也可以。

但是裁剪空间的就一定不行,因为裁剪空间涉及到了缩放,向量的长度发生了变化。

观察空间下的可以,其实这就非常简单了,因为关于近平面远平面的距离设置还有field of view angle这些东西都是在观察空间下设置的。

由于我们只关心这个distance,所以这里观察空间和世界空间谁都可以。不需要关心摄像机的平移旋转朝向等问题。

这部分Unity实现了封装:

然后就是把数据传递给shader,起作用的是那句SetVectorArray,会给shader里面的变量进行一个赋值。

注意shader里面的变量可以不写在Properties里面。

当然这里为了两边顺序不乱,在赋值的时候调整一下顺序。

这里把核心的四条棱的数据传递给了shader之后,剩下的工作就是shader处理了。

接受用的变量准备好,然后这里的宏和之气前向渲染类似。

这里的插值加计算已经说过了。

其实这里延迟渲染的雾,他是对天空和有影响的。因为他是直接对着渲染出来的缓冲区操作的,天空盒扮演的角色就好像是

贴在远平面上的fragment,所以这里正常被雾影响了。

之前的前向渲染,是在我们的pass中对每一个物体单独处理,而天空盒渲染的pass不归我们处理,所以没能影响到天空盒子。

通过这个分析,其实我们可以在延迟渲染中关闭天空盒的雾效。

进一步,可以使用宏来做一个判断。

上边的写法,如果在lighting设置中关闭了fog,得到的结果会是一个黑屏,因为这种情况下算出的factor全是0.

lerp之后就是雾的默认颜色,黑色。

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

Rendering 15

之前的延迟渲染,我们写的shader都只是把geometry给进行了render,但是对于光的shading如何计算这些我们shader中并没有写。

是因为我们默认使用了Unity内置的该类型的shader。

而针对这个计算,我们是可以使用自己的shader的:

写好了第一个shader之后,

会出现一个错误,说至少要俩pass。

也没说太清楚,HDR有点关系,我们补充上就可以消除错误。

这里说到了天空是用了模板测试,所以天空对应的fragment不会被光照shading影响。

另外关于,天空球,可能会被我们的光照的shader渲染到,这时候就是模板的缓冲区没有正确使用。

要想fixed it,在第二个pass中加入以上模板相关的代码。

这么干还不行,我们还需要把light buffer里面的数据进行一个转换:

不要写错了

相同的pass输出都是0HDRLDR一黑一白。。。。

我们的第二个pass是一些辅助作用,最核心的是第一个pass,第一个pass中要写的内容是光照的相关计算,

由于光照计算涉及的代码较多,所以这里直接写成include。

并且由于多个灯光走这个pass,轮流走,所以最后blend模式注意,同时写的时候肯定用到一些情况判断,然后相关的变体包含进来/。

有很多在第一个lightpass中了,我们只需要补充一个。

这个获取UV,其实从上一节课,我们可以看出,关于这个延迟的pass,就有些类似渲染一个quad,然后quad投影之后正好和屏幕大小相同。

这里是拿着屏幕坐标来当作UV来用的。

两个注意点,一个是屏幕坐标和UV之间可能由于API的差距导致y轴是上下颠倒的。

还有一个就是,计算好的屏幕坐标,要从齐次空间做一次投影,需要先插值再投影,因为投影之前的一切数据都是线性的,

插值这里需要线性插值。

下面一步,我们reconstruct世界坐标。这个就和我们之前的fog的思路是类似的,只有略微的差异。

首先这里就完全看作一个quad的渲染,然后我们之前的pyramid的四个轴,在这是被作为了法线传进来。

其实就是在放法线的位置上传递了进来。vertex的每一个语义都是约定好的,这里约定normal来传递四个轴。

(quad的四个顶点,每一个一个轴)

然后传递到ray。

之前我们得到ray之后,就拿深度对其进行一个缩放。

这块有一点不一样了,首先是之前的四条棱向量,是从pyramid的顶点,然后到底面的四个点,现在变成了pyramid的顶点到

Near plane的四个顶点,所以在使用depth缩放之前,我们需要先把向量进行一个缩放。

这里就是利用两个z之间的比例来进行一个缩放。缩放完成之后,再用depth来缩放:

之前我们到这里就停了,就可以求出距离了,但是这里我们想要求出世界坐标,还得继续往下。

首先这个最开始作为normal位置传递进来的四个棱,是摄像机观察空间下的向量,

所以我们要求的位置坐标,如果考虑观察空间下,就应该是摄像机的位置+向量,

现在摄像机的位置是原点,所以这里求出的向量直接就可以作为位置,这是观察空间下的位置。

下一步只需要变换到世界空间即可。

上边的UV和世界空间的坐标是我们需要自己准备的一些数据,有了这些,然后我们考虑一下怎么读取GBuffer,数据基本上就凑齐了。

之前写过四个GBuffer,但是有一个是用来写结果的,所以我们需要读数据的就只有三个。

读取也就是三张贴图,然后uv我们之前已经获取了。利用屏幕坐标得到的。

剩下的就是BRDF的计算了,但是计算之前,我们还需要准备一些数据。

值得一说的是关于反射率,这个他直接认为是高光Tint的最大通道的值,其实这个还是可以认为是白光过来,然后反射的

通道的最大值,认为是反射的量,然后就可以  反射/吸收 就是反射率

准备完毕之后就可以调用了。

上边并不没有对light仔细设置,我们这里单独封装并且对他们进行设置。

但是这里不计算自发光和间接光,至于为啥,因为我们之前已经算过了。

这个得从我们最开始写入Gbuffer的时候说起,第四个buffer我们写入的最终的颜色,当时由于考虑到自发光和间接光部分

是只计算一次的,所以不能在每一个灯的pass中去计算,所以当时在填写Gbuffer的pass中对这俩进行了计算,

然后填充到了第四个Gbuffer中作为初始值。所以在这里每一个灯光计算的时候,间接光部分只需要写为0即可。

上边把所有的geometry的数据准备齐了,对于走当前pass的光的数据存储在哪里?比如光的方向,光的颜色。

因为当前光在执行当前pass,所以直接从一些变量中就可以获取。

其实获取之后,给到BRDF就可以了。

这里光的部分需要revert一下

这里最开始命名少写了一个G,数据全没传进来,都是全白色的图。

接下来考虑Shadow部分,其实Shadow,核心就是找到对着已经生成的shadowmap进行采样,搞到手采样坐标采样就可。

之前前向渲染中,我们写Shadow的时候很简单,就是几个封装好的宏调用一下就行了,但是延迟渲染这块没有封装

所以我们需要自己做一些采样工作。

关于shadow map图的获取就是通过上边的那个变量。但是这里这个变量是有一点小小的要求的。

关于SHADOWS_SCREEN要想定义,必须满足以下条件:

(120条消息) SHADOWS_SCREEN宏打开的时机_wodownload2的博客-CSDN博客_shadows_screen

所以说只要它定义了,我们这里就是考虑平行光的阴影了。

注意采样阴影,采样的是屏幕空间的阴影,所以这里根本不要什么变换,直接就拿着UV去采样就行了。

然后这里采样这个操作我们还是得需要判断条件:

首先远了的物体考虑它投射阴影,那么shadow map上就得有他的一席之地,由于它比较远,所以shadow map上的它的分辨率

是比较低的,所以当其他的物体判断是否处在它的阴影中的时候,可能由于分辨率比较低,产生一种严重的锯齿走样等效果。

而且对于距离比较远的物体,其实他的阴影也没那么重要了。所以可以去掉。

而具体是多远的给去掉呢?这个就由上边的那个Shadow Distance确定。

实际的Unity的shader中,对于边界过渡,阴影从有到没有,是有一个fade out在里面的,而现在我们手动采样,并不具备这一功能。

所以我们要加上这一个功能。

首先我们获取这个fade的边界距离。

这里两种模式可能获取有些不相同,但是Unity帮我们搞好了。

有了这个距离之后,其实这个距离,也是对应着某种衰减,所以这里就是使用另外一个函数,来计算出对应的衰减:

这里俩都是衰减,相同的作用,都是越小表示衰减的越牛逼,而且他俩是一个叠加的效果,所以这里直接把他俩相加作为

最后的衰减系数。为啥不相乘,其实我们最开始采样的那个衰减系数,它本身是一个非0即1 的,如果这里相乘,说实话结果并不会太理想

甚至不怎么起作用。

关于这里使用的函数:

相关信息解释:

(120条消息) 阴影渐变衰减UnityComputeShadowFadeDistance与UnityComputeShadowFade_zengjunjie59的博客-CSDN博客

unity中混合光照与阴影的探索 - 简书 (jianshu.com)

说实话,看了之后只能说,他的值就是那么算的,好像有一些道理。仅此而已。

支持完了阴影效果,下面考虑LightCookie效果,这个其实本质上也是一种衰减。只不过是认为定制的衰减,

和之前衰减考虑的距离这些因素没有关系。

首先,还是需要先变换到light空间,然后在采样衰减,这里直接使用变换之后的坐标即可。

因为考虑这个cookie想要实现的本质效果,拿平行光来说,没有cookie的平行光,相当于一个巨大的面光源(这上边

的每一个点是一个平行激光,和当前light方向一致)。

然后如果考虑cookie,其实就是对这个巨大的面光源中的一个个小的平行激光亮度做一个调整。

调整可以是减小增大亮度。

所以最终照射一个大地板时,虽说法线相同,但是亮度各不相同。

所以说,采样应该直接拿他在空间中的位置就好了,而且拿xy坐标,因为默认光是指向z轴的。

而且我们所说的那个巨大的面就是平行于xy面的,所以我们其实采样采的就是这个巨大的面上面的激光的亮度。

我们就把他视作一个衰减值。这里巨大的面,其实就是由_LightTexture进行无限平铺得到的。

其实得到结果之后,可能会有一些问题,比如说,当我们的纹理比较小,而像素比较大的时候,这个如果不用mipmap

或者使用的mipmap等级较低(就是比较清晰的那种),就会导致采样频率不够,漏掉信息,产生artifacts/

至于修补方式,就是采用mipmap采样的 level偏移。

说白了就是当前像素应该采样某一个N level的图,现在通过指定一个值m,去采样m+N level的值,这样这里就可以让level

去选择更大的,然后效果就会更模糊,消除这种artifacts。

当然有些情况下可能需要更清晰的,来锐化图像。

但是这里作者整一个-8真有点想不明白,-8意味着选用更小的level,不就更加清晰,更加漏掉信息,产生artifact

(120条消息) tex2d tex2dlod tex2dproj tex2dbias区别_lylcn2003的博客-CSDN博客_tex2dlod

关于我们的场景,现在一旦换成LDR就会出错,关于这个纠正,只需要做两件事:

一是把混合方式修改一下,这里LDR他不会直接拿颜色累加的,而是相乘。

二是把颜色需要做一个decode。

这里说一下关于LDR的事情,就是先看下面:

这里是要把颜色输出,并且当前使用的是LDR颜色,所以需要按照LDR颜色进行输出。

也就需要一步,LDR的encoding操作,其实就是exp2.

然后再看下面,这个是把lightbuffer里面的LDR颜色给取出来,由于他是LDR的,所以这里拿出来之后要进行一个decode,也就是执行-log2.

咋说呢,虽说理解了,怎么encoding和decoding。

但是什么时候要encoding,什么时候decoding??而且是不是decoding之后的颜色就是HDR下的颜色。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值