学习笔记15

重复的fragment和vertex代码直接放在头文件中。

在头文件中写CG代码的时候,用不上写CGPROGRAM这些关键字的,其实头文件就是直接编译之前就展开的,可以理解为我们展开后他的上下部分已经写了。

直接添加进去一个新的平行光,对于我们的场景没有任何的影响,永远只有一盏灯起作用。

其实原因就是之前说过的渲染路径的问题,因为现在添加了一盏平行灯,按照设置,他也会是逐像素的,对于ForwardBase的pass,只有一盏逐像素光照的灯配用这个pass。且是最亮的那个。

其他的逐像素灯全都要使用ForwardAdd。

添加之后,这里还是只有一个光起作用,但是具体是谁起作用,这里其实是不确定的,当我们对光的颜色的Value,饱和度又或者是光照强度进行调整的时候,都有决定到底是哪个光起作用。

究其原因,这时候还是只有一盏灯起作用,是因为混合的问题,这时候实际进行了两盏灯的计算,但是后一个计算把前一个计算的结果进行了覆盖,所以显示的效果永远是第二个渲染的灯光的结果,所以说到底是谁照亮的结果,这个取决于谁是第二个渲染的灯光,这个灯光渲染的顺序和灯光的亮度强度都有关系。

不不不,上边的分析是错误的,我们可以通过Frame Debugger来查看渲染的顺序,其实一直都是先渲染ForwardBase的灯光的,也就是最亮的那个平行灯。两种结果的出现是因为两个灯之间的身份随着他们的亮度颜色等改变,也就是主平行灯的身份在他俩之间不断的切换。

这里我们需要对最亮重新做一个理解,最亮不是最大的intensity,而是加上颜色之后的亮度效果,不同颜色对于亮的影响也不一样。

比如,都属纯色,纯蓝色和橙色在相同的intensity和其他属性的情况下,和同一个红色的灯光去对比,蓝色的时候是作为add pass来渲染的,后者是作为base pass来渲染的。一切都可以在frame debugger中查看。

而且这里有一个强大的证据证实一定是先Base,然后Add,就是我们把blend给添加到Add的pass中。这样显示的效果一定是两个灯并存的。如果说先Add后Base,base没有混合的指令,默认就是覆盖,这就不会是两个灯并存的情况。

这里还有一个小的注意点,就是我们的深度写入要关闭,因为我们在第一个pass的时候,已经对这些物体渲染了一遍了,也就是他们的深度都已经写入到zbuffer中了,第二遍再渲染的时候,会重复进行一个深度判断,这里是会和zbuffer中的深度相等,然后就写入他的颜色,写入的同时还会写入深度缓冲区,但是这时候写入的值一定是和之前的一样的,所以完全没必要重写了,直接把深度写入关闭就行了。

首先按照他说的这里应该是4,2,但是我的是52.

从最开始说,最开始的值是51,这时候打开debugger,可以看到dynamic那一项只是画了两个立方盒子,也就是有一个没有被batch进来。

这个也不知道啥原因,解决方法就是给那个删掉,然后重新复制一个,就好了。

虽然上边成功了,但是实际情况和复制不复制没关系,之前那个不合群的也是复制的。测试了一下,其实他是和距离相关的,如果是距离太远,那就不能被batching一起,

(这里saved by batching的意思就是,一共三个batching在一起处理,可以认为是一个然后另外俩加进来了,这里的另外俩就是这个数字)

解决之后,这里还多了一个,打开debugger可以看出是clear,这个是和作者的版本不同导致的,可以看到他的debugger页面,

draw中是没有这一次的。

另外我开启和关闭阴影对这个数字完全没有影响。着实不知道为啥。

忘了说了,这里的数字什么42,52都是drawcall的次数。

另外当我们把两盏灯都打开的时候,这个dynamic batching就归0了,原因上边说了。

这里又一个小点,就是对于这些物体的渲染顺序,对于不透明物体,他是从近到远的,其实这样先渲染了近处的,等待会渲染远处的物体的时候,如果发现被遮挡就可以直接不渲染了,这样对于效率是一件好事情。

另外除了前到后影响渲染顺序之外,还有一些因素会影响渲染的顺序,比如,网格的类型,就是GPU渲染喜欢渲染类似的object together,类似体现在mesh或者same,先把类似的题都给写了,很正常的喜好,这一点的测试,我们可以通过设置几个距离近的球,然后在同一距离上设置一个球和立方体,看看他先渲染哪一个,结果是先球,因为和距离近的已经渲染的mesh是类似的。

这里添加一个点光源进去之后,效果很奇怪。

首先这个点光源走的是add pass,但是在add之前是走了一遍空的base pass的,这一遍就没有灯,所以这一遍下来都是黑的物体,

奇怪的是第二遍下来还特么都是黑的。注意我们的scene是红色的,但是game视图下是全黑的,通过debugger看到的也全都是黑色的。

还有一些奇怪的东西:

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

_WorldSpaceLightPos0

关于这个变量,其实是有很多细节的。

首先下面的文章,说出了:这个变量在Forward Base的情况下他是w为0,而其他光为非0.

学习"WorldSpaceLightPos0" - 简书 (jianshu.com)

而我仔细输出了一下,这个w不是为1的,我用的是点光源+forward add

首先从颜色上就不是,而且我又对比了这个变量中存储的位置信息,和实际大纲中inspector里面的位置信息,也不是匹配的,他俩之间差了一个四维到三维的投影,也就是除以w变成三维的。

而为了证实这一点,

扒开和他相关的一个函数,看看他是怎么使用这个宏的,对于这几个分支判断,我发现我并没有定义第一个宏,所以走的是第一个分支。

怎么判断的没有定义第一个宏,从下面可以看出,之前DIRECTIONAL这种宏是可以在变体中查看到的,既然没有那就说明没有定义了。

或者是就直接在shader中进行一个实验,也是可以证明,没有定义这个宏的:

那么说明确实是走的第一个分支,那么第一个分支,虽然没有对_WorldSpaceLightPos0这个变量进行四维到三维的投影,但是他对worldPos乘了一个w,这个的效果和_WorldSpaceLightPos0除以w来求一个方向向量是没有差别的。

更加奇怪的是,我们对WorldSpaceLightPos0这个变量进行输出的时候,每一个分量都不是0,每一个分量都是正数(通过调节世界中灯的位置)的时候,我们直接对于这个pos进行normalize,得到的结果就是一个纯0的数字。

这还有更奇怪的,上边那个是系统他自己写的normalize,不知道里面干了啥,我换成我自己写。

这时候xyz分量都是正数,都是输出直接白色的,sqrt之后的结果也是白色的,而且我还估计了他们的范围,然后他俩相除,直接就是0。输出黑色,输出黑色不是因为接近0 ,因为我给他乘以一个非常大的系数也还是黑色,加上负号也还是黑色,所以可以确定他的结果是0.和官方写的一样。

针对上边的fragment,我们看他的编译之后的代码,其实也就是这么除的,真几把不可思议!!!!!!

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

其实我们的scene视图下虽然亮了,但是效果也是不太正确的,就是因为上边,我们还是利用的之前的vert和frag来作为点光源渲染的pass,但是实际上应该换了比如说灯光的方向这个就和平行光不一样,而现在程序是直接让点光源使用平行光的计算方式。

所以看效果,根本不像是点光源。

但是关于Game视图下一片黑的问题,还是不知道为啥。

更秀的是,只要我们把

这一行,给改成点光源的形式,就直接ok了。game视图下就一切正常了。

这里处理衰减的时候,平方反比作为一个系数,会带来一个问题,就是d很小的时候,导致强度非常大。

首先从理论推导层面来理解这个点,因为我们的思路是求单位面积上的光子数,然后得到这个光子数是一个和距离平方反比的关系。

而当这个距离无限小的时候,意味着这个光子距离光源无限近,我们假设的是光源作为一个点,这时候表面积自然就是无限接近于0的,所有的光子都集中在这个无限接近于0的面积上,那么考虑单位面积下的光子数,不就直接无穷了么。

但是这个理论虽如此,但是我们在用这个公式的时候,就不必这么理想了,考虑物体和光源非常接近时得到的光照强度是没有衰减的,然后随着距离增加逐渐衰减,就是如上的公式了。

另外衰减是作用在光照颜色上的。因为无论漫反射啥的都会用到这个,光衰减了自然各种漫反射镜面反射都要衰减。

这里在确定光的范围的时候是有讲究的,我们需要和光的衰减之间做一个配合。这样在物体脱离光范围的时候,不会很突兀。

 

为了解决上边说的问题,其实就是做到一点:把range边界的衰减系数弄成0。

当然这个需要设计一个公式,当距离等于range的时候,得到的衰减系数等于0.其实这个设计还要基于平方反比,是很难的,等于0意味着必然有一个东西要达到无穷在分母上,这range又有严格的限制,所以做到这一点很难。

这里unity为了方便,他把一些东西都变换到灯光的坐标系下,并且进行了一个scale。scale的比例是range,这样range就变成了unit,所以只要有物体more than unit away from the origin,那就是超出了边界了,这样在计算距离的时候直接就是该点到坐标系原点的距离,有了距离在获得衰减的时候unity是拿着距离(0到1)去采样一个贴图,采样的结果就是得到的衰减系数。

这样贴图可以直接控制,当距离是range的时候,衰减系数就是0,同时帖图也可以按照平方反比来设计,这样最后就解决了公式的问题。

在使用unity封装好的宏的时候,注意传递的参数worldPos需要是三维的,妈的传错了他报错是个数问题。头文件注意放的位置,需要是再宏前面(注意我们这里的份文件编写)

当初直接添加点光源的时候,效果是有一些问题的,比如光的方向。

现在把点光源灭了,换成平行光,又会出现问题。

对于最亮的平行光,他走的是base pass,他获取到的光源方向有问题,另外一个走的是add pass,除了光源方向,衰减上也有问题,因为第二个pass直接define一个点光源的宏,进行点光源的衰减计算。

为了解决这个问题,引入变体:Shader Variants

首先这里说明了我们如何去定义变体,就是multi_compile语句,然后他说这个语句会定义一个keyword list,然后我们再keyworld list里面写了什么,他就会帮助我们创建相应的变体,即变体和这里的keyword是对应的。注意变体里面会定义keyword

重要的是下面的一句话,每一个变体,是一个独立的shader。

所以说就有了她下面的:

加上上边这句之后,光源执行pass的时候,首先根据光源类型,会找到对应的变体,然后变体就是shader,他会进行编译,也就是相当于会定义对应的宏了。这样就实现了,根据实际应用的光源来定义对应的宏,然后我们再根据宏的不同进行分支写代码,让不同光源在一些有差异的部分执行不同的代码。

上边说光源会根据他的类型找对应的变体,如果说没找着呢?

这里unity给出了答案,就是使用整个 keyword list的第一个变体,然后这个变体里面定义了什么宏就会定义啥宏。

这一点也很好去验证,

这里我们多写这么一个分支,然后我们把DIRECTIONAL变体的keyword给去掉,然后如果他走list中第一个变体,就会使POINT,也就是这里的第一个分支,否则他就会走else分支,根据效果进行判断即可。

现在所有的灯光切换的问题就解决了,因为光照方向的问题,利用分支判断就可以解决了,而衰减的问题,只要我们定义了相应的宏,他计算的时候会自动根据类型来进行相应的衰减,也就是我们使用的那个宏已经我们写好了分支判断了。

另外补充一句,作者只是在add pass中写了pragma_multi的语句,没有在base中写,这是因为啥?

测试了一下发现,把分支判断都保留着,把所有的变体都删除了,然后发现所有的光走的都是directional的分支

因为最亮的平行光一定走的是base,而base没有定义,坑你默认是directional,所以走平行光分支,正好就用不着写了。(现在先不考虑顶点和球鞋光照,因为我们光源数还没那么多,而且shader都是写的逐像素)

关于这里的高光的衰减着实令人惊叹!把透视投影用的出神入化。

他处理衰减的思路整体上和点光源是类似的,也是空间转换。

首先变换到以光源为中心的光源空间下,这里的光源是有指向的,在光源坐标系下,聚光灯指向自己的Z轴,那么只要变换后Z值是小于0的部分都被排除掉了,这个灯根本无法照亮他们,不需要计算。

注意,我们在计算atten之前,并没有从光源的视角下进行任何的裁剪,所以和点光源类似,先不管范围能否覆盖到,直接计算光照方向,并进行相关计算。而关于范围不能覆盖到一些物体即不能照亮物体的情况,以及光照衰减的情况都是一并在衰减中进行计算的。

所以我们要在衰减中解决那些范围外的物体,把他们衰减系数定位0。对于z小于0的可以一步排除,对于大于0的那一部分就是核心所在了。

上边说了,这个变换是一个透视变换,其实可以和MVP中的VP一起理解,这里其实就是先进行一个V变换,把光源弄到中心,然后再进行透视变换。

那么透视变换的目的是什么,我们思考之前的透视投影,其实目的就是投影,就是把frustum内的物体投影到一个投影面上。

那么这里就可以理解为:把整个圆锥内的物体投影到一个圆圈上,

对于裁剪,在透视投影中是根据投影之后的坐标进行的裁剪,可以理解为unit cube之外的都被裁剪掉了,如果我们只考虑xy轴的裁剪,那就是xy超出投影面范围的都被裁剪掉了。

这里也是类似的,投影到一个圆圈上,其实如果考虑z轴,那就是一个cylinder,但是这里先不考虑z,那就是一个圆圈,我们让得到的xy去对纹理进行一个采样,我们想要采样的起点原点放在图片的中心,那么就只需要让本来的采样坐标uv都加上0.5,然后根据xy采样,采样下面的图:

如果说xy采样到了黑色的部分,那么就意味着,xy比较大,其实这个图是经过设计的,和投影的圆圈是对应的,这里xy采样到了黑色就意味着该点已经超出了投影的圆圈的范围,意味着这个点根本就不在聚光灯的锥形内。

所以说这样我们就完成了该点是否在锥形内的判断,对于聚光灯,锥形的某一个圆形截面,整个截面上他的亮度差异并不是很大,从效果上我们也可以看出来:

所以最终关于衰减的计算,其实就是拿该点距离坐标原点的距离来当作一个uv去采样一个衰减的纹理(这一点和点光源非常类似。)

记得刚开始加入聚光灯的时候,他也有范围考虑???

就是当程序中只有平行光的变体的时候,我们考虑聚光灯或者是点光源加入到场景中,这时候,按照逻辑,应该这俩都会定义一个宏DIRECTIONAL,没错,我们可以做一些判断证实这一点。

然后按理来说,到了光方向赋值的分支那里,也都会按照平行光的方式来得到方向,这一点也是没错的。

最重要的一点是:对于attenuation的计算,也就是衰减的计算,前面我们通过分析聚光灯的衰减,其实里面是蕴含了剔除的操作的。因此现在改成了directional的衰减,他根本就不衰减,那么更别说蕴含剔除相关的操作了,所以说显示的结果理应是没有剔除操作的额。

但是实际结果并非如此,实际结果是正常裁剪的,是和定义了SPOT变体情况下的裁剪一样的。

所以这里有证据怀疑他在衰减计算之前,是有过裁剪操作的。

而在介绍点光源的时候,作者说了一句,要把范围和衰减搞成同步的,这样才不至于物体在走出范围时,身上的灯光有一个突变。

这里说的意思,好像也是在衰减计算之前有过裁剪操作的。他会先判断哪些在范围之外,直接不对其进行计算。

而又因为我们定义DIRECTIONAL宏的时候,两个光源都仍然有自己的这些裁剪操作,所以说这个是根据光源类型直接来的,而不是根据宏。

而且这样的话,在执行衰减之前,执行一个裁剪,衰减本身又带有裁剪功能,这不矛盾吗?

其实想明白了就不矛盾了,其实都是和点光源类似的,聚光灯也有裁剪,他也是为了不出现突兀,裁剪是因为他想减少渲染的物体个数,如果不率先裁剪,那么进入到衰减阶段进行裁剪,此时已经是fragment shader里面了,那就多计算了非常非常多的内容,如果先裁剪,那么很多物体就根本不需要去考虑能照亮他。

所以聚光灯和点光在这里是完全处于相同的目的。

聚光灯看似不需要这种裁剪,这种取消突兀变化的裁剪,但是实际还是需要的,当我们没有定义SPOT,也就是没有衰减的辅助的时候,其实一个物体在灯光范围边缘徘徊的时候,他是突然整个被照亮,整个变暗了,是有这种突变的。这里整个被照亮,其实就是DIRECTIONAL的attenuation在起作用,那个不会裁剪,只要是在照亮范围内,也就是被当前光考虑,那就直接完全照亮。

而考虑了正确的衰减之后,在边缘进出的时候,他也是有一个渐变的过程的,而且不会是说整个物体亮着两者突然随着距离变化黑了。没有这种情况。所以这就是用衰减来考虑裁剪的作用。

之前作为mask的那张图,用来裁剪投影的圆圈以外的物体的,他的本质就是一个单通道的mask,我们是可以自己去定义这个图的。然后指定到光源的cookie参数上。

这里对于平行光的cookie,首先它加上cookie,其实就是我们对他的光照做的一个衰减。只是这个衰减无关乎距离。是根据贴图的形状,来对光照的对应部位进行一个衰减。

所以这里实现它还是通过衰减的方式,因为这种衰减方式不同于之前点光源,聚光灯,所以他有他自己的衰减的宏。其实这种情况,他就被Unity视为一种新的光源类型了,他有着不同的衰减,需要有自己特殊的衰减处理方式。

他不是平行光,所以他就不会使用base pass来渲染,那就always additive。新的类型,也有他自己对应的keyword

其实上边采样可以看出来,就是认为一个平行于xy平面的无限大平铺的贴图存在(light空间中,光线指向z轴正方向),然后根据照亮的点的xy坐标来对这个贴图进行一个采样。

他这里的cookie类型实际需要的是cubemap,所以直接把贴图的texture shape改成cube即可。其他不用动。

这个也是完全类似的,我们可以认为他是一种新的光源,然后他也有自己的Attenuation,有他自己对应keyword,这里unity又帮我们做了一件事,由于keyword越来越多,这里看上去并不整洁,所以unity帮我们把这些常用的keyword封装在了一起,只需要换成下面的这一句即可:

这个采样就更简单了,需要注意的是应该同时考虑实际的点光源衰减。

在多光源的情况下,像素灯的数量越多,会导致drawcall的次数越多,所以说这必须要有一个上限,上限就是:

这里设置默认是4个,如果实际的光源比设置的这个值要多的时候。

假设物体处在五个灯范围内,但是对他来说,最多四个能够逐像素照亮自己,所以说他会根据intensity和distance来确定哪四个是逐像素照亮自己的。

但是这回带来一个问题,就是运动的时候,这四个灯不是恒定的这四个,有可能会换掉其中的一部分,而在灯光切换的时候,带来突然颜色亮度上的变动,这是bad的。所以就引入了顶点照明

这里没有明白他说的the other option指的是什么?

这里似乎是指的base pass中的核心部分,原始的部分,你看我们的顶点光照,他虽然是走整个base pass,但是它并没有去碰原始部分的代码,而是只计算了#if defined(VERTEXLGHT_ON)的部分。

说的另外一种选择是否是原始的代码部分???这部分代码其实始终是只有主平行灯才执行。

因为这块的一个关键词其实就是一种灯光类型对应的流程。这里所说的另外一种选择应该就是第一个pass对应的一种流程,这里只可能是主平行光走的那个流程,而且它确实不需要什么关键词。走流程的时候就默认他是平行光了。

这里单独封装一个函数用来计算这个顶点着色结果,然后插值到fragment中之后,我们需要把这个结果累加到其他光计算的结果上去,这里把他当作间接光的diffuse去累加。

这里是利用unity_LightColor[0]来获取光照的颜色,这里是数组是因为它受到不止一个点光源的影响。

注意这种逐个顶点的光,只支持点光源。

上边只是一个测试,如果我们想要对这些个点光源,也进行diffuse specular等等的计算,也是完全ok的,他的变量来源

这里就是保存好了的灯光位置变量。

这里还有衰减相关的变量。

这里不考虑高光是因为,考虑了又插值,效果会很差,尤其三角面比较大的时候。

之前对于多个点光源,我们只考虑了[0],也就是最近的那一盏灯。

现在都考虑了,其实就是重复之前的步骤四次,而这里unity又帮我们封装好了函数。需要的参数就是:光的颜色和位置,坐标和法线,还有衰减。

他这里的衰减是一个float4,它里面四个分量就对应了四盏灯各自的衰减值。

关于这个函数内部的实现,稍微有些不同的是normalize,

首先看最末尾,我们可以看出,他的每一个计算都是存在一个通道里的,也就是第一盏灯相关的一直都在x通道中计算,直到最后diff.x,所以看的时候盯着一个分量就行了。

主要的是关于normalize,这里的corr是对距离平方的一个rsqt操作,得到的也就是距离的模,然后乘以ndotl,这个被乘的ndotl其实是用标准化的normal和非标准化的toLight相乘得到的。所以只需要把结果除以toLight的模就行了,就变成两个标准化相乘的结果了。

这里的意思可能是像素灯光和顶点光一同作用的时候,这两种光在效果上需要一个过渡,过渡的方式就如上所说。

但是这个过渡真的很迷???

难道是一个逐像素照射到了左边,逐顶点照射到了右边,直接看上去两边对比有些大,所以搞一个过渡???

这里还说了对于这个函数的使用,如果顶点光少于四个,那就会把少的那部分当成黑色的光来计算。总之计算的次数不会减少。

这里可以手动进行修改灯光的重要性,来对灯光的逐像素和逐顶点计算方式进行一个控制。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值