学习笔记17

Rendering6

 

上边的思路是把面进行一个细分,细分成很多的顶点,然后我们可以实际的移动这些顶点来形成bump效果。但是这天然需要很多的顶点才能有这样的效果。

所以这里就把顶点层面改到了像素层面。使用纹理高度图来完成这个效果。

注意这种nooffset习惯

这么来用的话,does not work。

首先这里求法线的方式是通过先求出切线。而本身对于一个三维空间下的高度图,他的切线是有无数条的,因为他有一个切面。

因此这里忽略掉一个维度,忽略掉v维度,也就是考虑当前像素的切线的时候,只考虑和当前像素同一行的所有像素,也就是组成的一条曲线,然后去求曲线上点的切线。

上边的图,纵着是表示h,横着是表示u轴。这样v轴被忽略的情况下,切线就变成了唯一的,而切线就是导数,自然可以用finite difference来求。(也就是导数的定义,有限微分,然后考虑极限情况下就是导数。)

注意Unity中,y轴是朝上的轴,所以在高度图贴到我们的平面上的时候,相当于求切线是在xy平面上求的,所以这个2D切线换成3D的时候,Z轴的分量保持为0即可。

这里还有一个trick,就是巧妙利用要初始化,所以把整个向量同时乘了delta.x,这样就不必除了,因为除法不是正好整除会损失精度。

得到的效果,太明显了,这就不太好了,因为这个毕竟是perturbed的法线,所以效果太明显太容易穿帮了。所以收敛以下。

回想我们的切线求的方式,是用的高度差除以像素宽度,这个斜率作为切线,我们把像素宽度那一项增大就好了,这样效果就会变弱,这里直接改成1,其实就是h2和h1本身差距不是很大,这个值本身就接近1,所以切线倾斜度有那么一点就够了。

上边效果太黑,其实是我们把切线当成了法线带来的结果,视觉效果当然是错的了,从切线得到法线,因为这里的切线必然处在xy平面内,所以我们直接进行一个旋转就好了,逆时针旋转90度。因为我们的切线是条线,我们取的向量方向是向U轴正方形那边的,这里应该逆时针转才是法线。

2D的旋转结果还是容易写的。

刚刚的切线考虑的时候是拿当前的去和右侧的像素点来求的,并不是完全利用当前像素的信息,而求当前像素的信息,其实和它右侧的那个像素点其实是没关系的,想想图像曲线的切线代表导数就知道了,所以得到的结果是有偏的。

所以就有了上边的计算方式。这里是把求切线全都center on当前像素点上了。

这里我们有的是一个二元函数,二元函数的图像是有偏导数的,刚刚我们求的算是h对于U方向上的偏导,所以只拿这个作为结果还是不太合理的。所以我们可以再求一下V方向上的偏导,这次直接center on当前像素。(此时就是不考虑x轴了,考虑yz轴。)

获得两个偏导数之后,直接叉乘,就得到了法线。

在算法线的时候,还是需要注意下方向的问题的。

由于这里的叉乘操作很多项是0和1参与的,所以最终的结果还是比较简洁的,所以我们可以直接写,而不是用cross function。

注意从头到尾,这个height map的做法中都没有提到空间变换的事情,其实是用不着变换的,因为我们的平面是完全平方的,我们思考的方式也是UV直接贴到我们的平面上,就是在世界空间下思考的,所以直接拿他得到的切线也是世界空间下的。

这里在实现的时候,注意我比作者多了一个负号,也就是说如果我把texel_size替换成1的时候,在考虑v方向的时候,我应该替换成负1.

具体的原因是windows平台下,编译使用的是dx那套坐标系,也就是uv他是以左上角来为原点的。

而实际的世界坐标系,unity使用的是左手坐标系,也就是距离我们越远z值越大,其实这俩坐标系有些相反,因为uv坐标系是距离我们越远V轴越小。

而实际应用的时候,我们是直接拿uv的值作为世界坐标来用的,所以求出来的向量分量,在Z分量上是应该娶一个相反的。

叉乘是右手定则。

作者没有加,它的效果也是正确的,但是我俩其实还有一个差距,就是一共这俩差距给抹平了,所以效果都是对的。

另外一个区别就是他的cross写的是v叉乘u,而我写的是u叉乘v。

因为作者的坐标系是左手的,也就是说他的x轴是朝着左边的,yz和我们一样。

他推导的这个结论,因此我俩也是不太一样的。区别就在于我要把第一项的1改成-1,

最后的结果,需要把fv那一项改成正的。

这一点在后面有使用到。

上边地面是xz平面,也就是图中的小平面是倾斜放置的。这里思考一件事情,我们上边的高度图都是应用在一个躺平的平面上的,然后我们根据实际平面的位置去写出hard code的法线计算。

那么如果我们应用在模型上一个高度图,模型的面可不都是躺平的。现在假设其中一个倾斜的shading point,一个fragment就是上边的小平面。

而且我们假设它对应的高度图部分,那一片都是一个颜色,也就是同一高度,按照之前的思考,我们求出来的两个切线就应该是xz轴,自然法线就是y轴了。但是这里明显不应该是。

其实原因出在哪里呢?首先高度图我们记录的是深度偏移对吧,刚刚地面的高度本来都是0,而算切线的时候,他们的y都是按照高度图里面的值进行计算的,所以说记录的是深度的偏移没错。

其实这里我们最终算的是这个小的fragment投影到xz平面上的那个小面的法线。本质的原因就是:高度图在使用的时候默认当前fragment是平行于xz平面的。然后他直接拿高度图中的值作为y,其实这个我们认为是深度偏移也好,认为它不是也好,反正用的时候就是拿两个高度图里面的值作为y相减,而在求切线的时候,具体的y根本不重要,重要的是delta y,而默认我们的fragment平行于xz,也就是y值已经等同了,所以你认为它是深度偏移也好,认为他是固定的一个数字然后用来这种计算也罢,其实结果都是一样的。

那么我们要想依旧使用高度图这套方法,那就需要把他的默认变成真的,有一个办法,就是把当前fragment变换到局部空间下,让xz是和平面平行的,而这里的xz就是和uv对应的,而这个空间就是切线空间。我们在这个空间下可以依旧按照相对高度记录的东西来计算相匹配的法线。

还是按照之前的说法,我们直接把高度图代入到这个坐标系下求解,解出来的法线就是局部坐标系的y轴,这也确实是对的。

至于实际的切线空间是z轴上,xy和uv分别对齐,和这里说法有些不匹配啊,其实这就很简单解释了,如果我们学习高度图的时候,把上面的这个大地面,改成和xy面平行的,然后去得到一个凹凸不平的结果,这时候的算的方式就稍微改动一些,而按照这种算法,就是默认该面是和xy平面平行的,所以可以变换的时候让xy面和uv对应,z轴是朝上的那个。

这里还有一点可以解释,就是z轴为啥是法线,fragment的面和xy平行了,不就自然有了原始法线即z轴么。

(这里本来想说法线是一个三角形共用一个切线空间的,其实这个并不正确,因为fragment的法线都是插值得到的,构建切线空间时unity的tangent也是插值的,所以这样能够保证建立空间时,uv轴对应xy平面是符合高度图用法的。如果法线插值,切线不插值,甚至构建的坐标系都不是正交的)

当我们把高度图应用到真实的模型上的时候,不管你的模型再怎么变,各种动画都无所谓,我们每一帧计算出来的fragment在切线空间下的法线都是一样的。

所以这个工作,每一帧都在重复操作,为何不提前做好,然后直接采样获取呢?

于是normal map就出现了。

这里说了一点,之前完全没有注意到的,之前mipmap的时候,我们知道像素放大了要用mipmap,缩小了要玩插值。

这里复习下:放大了,可以直接选最近,可以选了最近的level再插值,可以两个level都各自插值,最后level之间再插值。缩小了只有选最近和插值。(就算勾选三线性插值也没用)

我们之前思考的时候,都是基于颜色的,颜色的贴图是怎么进行mipmap的,是怎么插值的。

法线这里也是有mipmap的,这也就意味着,我们vertex shader传递进来的normal,有可能是插值过的,所以不一定是normalize的。

但是最后一句,好像又说了,unity会帮我们干这个事情。

这里unity内部好像是可以从一张高度图转换到normal图的。

这里就说了我刚开始谈为啥z轴是法线,而不是我们讲的y轴,其实是谁都可以,这里区别就是把他俩swap一下。

那么我们在采样之后也swap一下。变回到之前的标准下。

总之来说:首先这个贴图里面存的是根据高度图计算好的法线,只是有一些轴等等的变动,我们只需要变动回来即可。我们采样,得到的是颜色,颜色*2减去1即可得到法线,但是是一个轴swap之后的法线,然后swap得到的就是轴变换之前的了,其实就是和我们之前那种算法算的法线一样了。(其实这里唯一的不同是空间不同。)

但是空间对我们一个地板的例子来说没有影响,他的切线空间和世界空间是完全aligned的。

DirectX Texture Compression 5 - Valve Developer Community (valvesoftware.com)

大致知道压缩的位数和商标说的usage就好了,具体咋压缩的,以后再说吧。

S3 Texture Compression - Wikipedia

什么是DXT压缩?详解UE5材质每种压缩格式!如何选择适合项目压缩格式? - 哔哩哔哩 (bilibili.com)

(119条消息) DDS的DXT5格式解析_春夜喜雨的博客-CSDN博客

这等复杂的解压操作,unity自然又要封装好来喂我们咯

新版本的:

我们可以看到这里有判断一个宏Shader_Target

需要3.0以上才能使用这个scale。

默认情况下shader是2.5的,所以我们需要写一个大于3的,才能够让这个scale生效,否则就得自己手动让他scale。

这是两个normal直接相加的结果。

右边两个是单独的normal和detailNormal。而左边是相加之后的结果。效果显然两个都变的flatten了。

所有的设置保持不变,把计算的方式改成导数相加。就得到了左边的图,右边的是上边的图。

这里之前说过,推导出的叉乘结果,我俩差一个负号,但是其实对于这里没有任何的差别,因为最后就是把导数相加,如果前面有正负号都可以提出去,并没有任何的影响。

上边那个在flatten的时候效果比较好,在steep的地方,稍微差点意思,但是视觉上真的差不太多。

所以又研究出来这个方法,whiteout blending。这个他说放大了xy,同时让陡峭更明显,flatten时依然两个互补影响。

当然这个whiteout blend也被封装了一下。

还是多少有些区别的,左边是之前的,右边是whiteout。

Detail normal至此完成。

 

这里就到了我们之前所说的,我们从normal map中得出来的法线结果,就是高度图计算的结果。

而高度图那种算法必须基于一个前提,就是当前的fragment是平行于XZ平面的。

那么如果没有平行,其实算出来的结果就是假定平行的,其实也就是假定处在另外一个坐标系下算的结果,所以我们想要得出真正世界坐标系下的结果,就需要做一个空间的变换。

变换之前,我们要找出来它假定的这个空间的表示方式,然后才能实现空间之间的变换。

unity内部存了一个T向量,也就是vertex data里面的数据,这个T向量是和当前fragment的U轴是对应的。而我们的假定的空间的x轴其实就是U轴。所以说这里就可以确定U轴。

然后法线其实也就是假定的空间中的Y轴,所以叉乘一下得到第三个轴,我们整个空间就确立了:

这里乘以一个-1,本来是认为左右手坐标系的问题,但是并不是。

首先这里坐标系其实也是有问题的,因为右手系x叉乘y等于-z,而左手是等于z的,所以说这里他俩通过俩轴计算第三个轴的时候计算方式本不该一样的。

所以左右手轴也是有问题的,但是这个问题并不是在这里的-1解决的,而是:

unity在帮我们生成切线的时候解决的,因为模型导入unity的时候,其实会考虑模型的左右手空间和unity的左右手空间。然后unity会根据自己的左右手空间生成对应的切线。

这样坐标系不一样的时候,生成的切线也不一样。

然后这里为啥还要考虑这个-1:

其实是镜像的问题,就是模型本身的法线和切线有一半是镜像过去的。但是如果你用这俩镜像的直接叉乘,你会得到的是镜像的副切线,那这两边就完全镜像对称了,一个模型的不同顶点的切线空间,居然有左手也有右手,这就奇怪了。

所以模型就在切线中藏了一个数,有的是1有的是-1,1的那一半直接叉乘就是第三个轴,另一半的需要叉乘之后反向才是正确的第三个轴,副切线轴。

做了下法线的可视化。

都是C#的知识。

这两段,好像说了啥,好像也没啥用。。。。

在shader中应用的时候,注意这里w分量,最好是按照上边的写法,只对xyz变换,然后w再填上去

其实这里下面的normalize写法,其实就是一个变换矩阵*切线空间的法线。

这个变换矩阵是TBN三个向量按列放进去的。(标准的空间变换矩阵。)

这里有俩点要说:一个是上边的是否需要normalize,其实确实用不着,因为我们的TBN矩阵变换,我们只需要考虑其中的旋转部分,因为我们用这个矩阵变换的对象是向量,只考虑方向。

另外一个点是关于顺序问题,这个上面相乘的式子,我们并没有考虑YZ的swap。我们要在变换之前进行一个swap。

首先为啥要swap,是因为我们在从高度图计算出法线的时候是认定Y轴朝上,然后平面和ZX面对齐的。

而常见的map是认定Z轴朝上来生成的。计算的方式和我们不同。应用的场景也是不同的,也就是说,如果想要应用他的map,应该把z轴朝上,然后平面对齐xy面。因为他就是这么个空间求出来的,如果应用的空间不是z轴朝上,那就需要对得到的这个结果进行变换,变换其实只需要把所有的z坐标和y坐标互换,所以也就有了我们的swap。

其实这里本质就是一个空间变换。

那么整个TBN变换就可以有两种理解:

一种就是我们的map他是高度图求的没错吧,他对一个倾斜的像素求的高度图,由于求的时候要求是Z轴朝上,但实际不一定是z轴朝上,所以我们构建一个局部空间,求出了这个法线。求的是局部空间的,我们要把它变换到世界空间才能使用。也就是两个坐标系之间的变换,只需要做好对应就好了,它的U是x,也就对应世界的x,z是对应z的,所以变换矩阵的书写顺序就有了。

还有一种理解就是先应用到我们以y朝上的应用场景下,需要调换yz,然后再从这个,和上边类似的理解,需要变换到世界空间下。这个对应是不同于上边的。这里注意,这里x一定对应x,y一定y,z一定z,不同的只是谁代表y谁代表x,这里就是normal代表y,而上边是normal代表z。这样会影响变换矩阵的书写。

最终形式如上,第二个写法,我们看他说了拜托YZswap,并且他也把yz的swap那一行给划掉了。这个就是我们第一种理解的方式去理解,直接进行变换的。

他就是t轴对应x,y对应b,z对应n。

而上边那个肯定是上边有一行交换的代码,交换之后,我们的x对应t,y轴对应n,z轴对应b。

最后还有一个小的说明。这个算是挺罕见的了。

这里就是建高模,然后烘焙到低模,对于法线最开始获取,是在模型的空间里面的,不是切线空间,是需要从模型空间转换到切线空间然后保存,烘焙到贴图,使用的时候就reverse了。

既然这么来回转换,一定确保转换的算法等等的是一致的,一旦出错最后进入游戏的模型就wrong。

所以这一切都是需要同步的,也就是这里的同步切线空间的说法。、

关于我们的模型的切线在导入unity的时候是使用mikktspace算法来生成的。

这个算法,我们可以选择去fragment计算副切线还是vertex shader中去计算。

也可以按照最后写的这样,自己两个都写上,然后自己定义一个宏来作为判断条件,通过是否定义这个宏来控制程序是逐像素还是逐顶点。

===============================————————————————————————————————————————————————————————————————————————————————————————————

Rendering7

阴影现象中的一种说法,cast阴影。

就是真正的soft shadow。unity在他所用的版本中,还没有这种技术,只有filtering生成的soft shadow。

 

这个显然是不可行的,因为每一个物体表面的fragment,都去和光源相连,然后检查是否和场景相交。

这里看右边,我们可以看出,他最先来了一个depth pass。也就是从摄像机视角的深度。

然后朝下的三角算是目录一样的东西不用看,看朝右的三角形才是具体干的事情,他又来了一个shadow map的渲染,这个是从光源视角下的深度,

这里扯了一句normal map

这里就有意思了,在获取shadow map的时候,使用的是正交相机,还说相机具体的位置并不重要。

然后每一个平行光处,都生成了四个shadow map信息,更有意思了。

首先为啥有四个是和我们的设置有关系的,我们自己设置了four cascades。

还有他说四个象限都在不同的观察点渲染的。

想一下正交投影的原理,他的原理就是把摄像机放在无穷远处,然后拍摄一张图片,这么说好像他的位置确实没有什么影响,有影响的只是这里最终看到的范围多大。这可能也是这四个的唯一区别。(看看图上的小球,明显可以看出大小都是相等的,没有什么近大远小)

注意,由于我们一个是从camera深度图,一个是从light的深度图,这俩所采用的变换方式不一样,裁剪空间自然不一样!!!!

Unity中的ShadowMap - 知乎 (zhihu.com)

Unity中的shadows(一) - 知乎 (zhihu.com)

Unity中的shadows(二)cast shadows - 知乎 (zhihu.com)

Unity中的shadows(三)receive shadows - 知乎 (zhihu.com)

Unity中的shadows(四)collect shadows - 知乎 (zhihu.com)

我是真的想学图形学 - 知乎 (zhihu.com)

大部分都是这么说的。

之前有些不理解,为啥是非0即1的,首先这里是只考虑直接光的,间接光那些是待会考虑的内容,包括天空等等的环境光部分。所以在这一部分上,把阴影的部分颜色直接写黑,完全没问题,

Unity实时阴影实现——Screen Space Shadow Mapping - 知乎 (zhihu.com)

这里有一个把这套算法自己实现了的。

思路反正都是上边所说的那样。

相关文章:

Unity实时阴影实现——Shadow Mapping - 知乎 (zhihu.com)

Unity实时阴影实现——Cascaded Shadow Mapping - 知乎 (zhihu.com)

Unity实时阴影实现——Screen Space Shadow Mapping - 知乎 (zhihu.com)

还有几个PBR相关的,博主还有UE相关的文章

Unity的PBR扩展(二)——PBS代码剖析 - 知乎 (zhihu.com)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值