永远相信美好的事情即将发生 😊!

卡通渲染之头发高光(anisotropy)的实现(URP)

分享 Mavis 25℃ 0评论

其实这张的标题不应该加卡通渲染,毕竟这部分内容是针对头发的高光处理的,没有特别针对卡通渲染……但为了和前面一篇保持格式就这么写了

ok,回到正题,接下去的内容依旧是我自己在学习了那么多后,对头发的高光效果如何实现的理解。

 

最常拿来做例子的材质就是头发了(忽略它的粗糙和杂乱),先上一个真头发

我们可以看到在红色方框中有一圈高光,然后再看看我桌面的小美女头发高光的样子

话说写到这里,我觉得我第一句话说错了,哈哈哈哈哈,还是有关系的嘛,毕竟卡通和现实的效果是相近的,没有其他效果相差那么悬殊

好了,至于各向异性是啥,我就不讲了,网上有很多人的解释和分析

重点在于如何在有限的空间信息下模拟出这样的效果。

首先看一下我上篇文章做出来的效果:

可以看出来高光是由一个点为中心然后以圆进行扩散的

specular = light.color.rgb * _SpecularCol.rgb * lerp(0, 1, smoothstep(-w, w, NdotH-(1-_SpecularScale))) * step(0.0001,_SpecularScale);

但是各向异性的材质下就不是了,比如在米哈游的技术分享中有这么一张图:

可以很明显看出来高光变成了环形,且有发丝的感觉,纵向上处于同一圈内的点可以当做是一根头发,计算出每根头发的高光效果,纵向环绕一圈后自然就成为了圆环,还要注意因为发丝是从顶部开始分的,所以高光不会像之前做出来的那样能随便挪,一定是要绕着头顶的

先来看一下用 Kajiya-Kay Model 实现头发各向异性高光效果的关键代码吧:

      float StrandSpecular(float3 T, float3 V, float3 L, float exponent,float scale)
			{
			    float3 H = normalize(L + V);
			    float dotTH = dot(T, H);
			    float sinTH = sqrt(1.0 - dotTH * dotTH);
			    float dirAtten = smoothstep(-1.0, 0.0, dotTH);
			    return dirAtten * pow(sinTH, exponent)* scale;
			}

ok,半角向量不能少,这里的T实际上是使用的副切线,为什么要使用副切线?

因为这个片元被模拟成一根极其短的头发(因为头发是圆柱形,所以最终近似看成一个圆片),要知道按照之前使用的Blinn–Phong reflection model是需要法线值的且是关键属性,而该片元实际能获取的法线属性只有一个,但是被当成圆柱形的发丝后,存在着n个法线,法线的不唯一性导致不能再简单的用一个固定法线值去模拟了

我们需要寻找一个能够唯一确定的值,法线不确定->切线也不确定,但是法线和切线所在平面是确定的,根据副切线的算法,可以得知副切线是这个圆片唯一确定的,所以唯一能利用的空间信息只有副切线。这就是最后会选择使用副切线的原因。

在选择了副切线以后,我们需要做的是还原当初直接使用法线进行计算的过程。

首先当时要计算的是NdotH   ,而现在只有一个向量T ( 就是bitangent ,因为代码里用了T,这里就统一用T来表示) ,可以自然的想到计算TdotH,因为我们知道T和N肯定是垂直的,所以计算出来的这个值跟原来的值有一个相反的关系。sinTH 的计算就是来将这个T的反给整回去,再到后面的内容就是一些技巧性的内容(和之前一篇里用到的specularScale这个值一样,技巧性的调节效果);

今天就先大概说到这,明天再详细分析。

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

又到了新的一天,继续分析昨天匆匆讲过的内容吧,想了很久该怎么画这个图:

当片元还只是片元的时候(也就是对于法线性质来说各向同性的时候)其实代表的是它本身,所以可以直接使用它自带的属性进行计算,即使一个片元很大,那么只有一个法线属性,这个片元上所有的点都是用的这个法线值,所以不存在有不同取值的情况

但是现在需要片元代表一个从空间上来说和它是垂直的圆,就需要注意,属性的变化:

从上图就可以看到效果了,实际上这个片元所持有的法线属性只是所要代表的圆身上的一条法线信息(假设圆的法线属性和片元的法线属性在同一平面),那原来使用法线计算高光的办法就没用了吗?当然不是,只是不能再使用片元的法线属性了,因为这不能真实模拟圆了。

ok,那怎么用呢,总得找个值代替原来的法线去计算吧,之前为什么能使用法线其实我觉得还有一个原因:法线这个属性对于该片元来说是各向同性的,也就是不管光线视线角度怎么变,法线就这一个方向。

我们需要使用片元的信息去模拟出一个在圆上也是各向同性的属性才能加入计算。先在圆上找找有哪个属性是各向同性的呢?

当当当当,也就是圆的“法线”(只是对于一个空间中的圆来说常常这么定义,但放到头发中该值不应该叫做法线):

这个值对于这个圆来说是不变的,就像片元的法线一样,所以我们需要通过片元的信息来计算出它模拟的圆的这个属性值

经过昨天的分析已经知道这个值就是片元的副切线值了

总结一下选取副切线计算的原因:需要找到个各向同性的值去代替之前各向同性的法线进行计算。

好了,接下来就是如果使用副切线的值计算出该点的高光效果了,这部分我就不画图了,直接用网上的图吧(需要解释一下图里的T就是片元的副切线值,加上这张图和前面的分析,应该能理解的比较清楚了):

其次还需要知道这张图上的法线跟片元的法线属性没有关系(虽然可能重合),这张图上的法线是圆片上被照射到的这个点(圆的边界上的点)的正确法线信息。这个信息我们之前说过了是不知道的,之前的算法是NdotH,不知道N没关系,就像打几个电话你就可以找到世界上的任何一个人一样,N和T有关系的呀,N垂直于T,也就是NdotT = 0 ,也就是不管正确的法线值到底是多少,它都跟T垂直,它跟T的相对关系是不变的,本来是要计算N和H的夹角cos值,现在可以先计算T和H的夹角cos值,H离N越近,就说明离T越远,这就是我之前说的两个值有一个相反的关系。

好了,虽然计算了TdotH,那怎么计算出真正的NdotH呢?

首先我们还需要知道这个图中的向量之间的空间关系:LVH处于同一平面,TN处于同一平面且T垂直N,除此以外是没有相关联的信息的。

 

 

把关键的向量信息抽象出来(H可能是经过这个点的任意一个向量(跟视线和光线有关)),然后我们现在只知道θ2,对于T来说,只知道这么一个角度是确定不出来H的位置,同理T也不知道N在哪儿,也就是说THN这三者的关系实际上是这样的(N和H可能指向圆圈的任何一个点,准确来说对于H,那个圆可能在其他地方,我这只是画某个情况):

好吧,看来这电话是打不通了,但是通过这样的关系我们可以退而求其次,毕竟只是需要模拟一个效果,只要看上去能对那就是对的,所以唯一我们能算出来NdotH的时候只能是NHT处于同一平面!!,也就是如下:

这个时候我们知道θ2,还知道θ1+θ2=π/2(当然也可能是θ2-θ1=π/2,当H在左侧时候),ok,说到这,是不是恍然大悟为什么要算一个sinTH了

回到代码上看看:

float dotTH = dot(T, H);这句就是用来计算cosθ2的,不多解释

float sinTH = sqrt(1.0 - dotTH * dotTH);这句就是我们要算cosθ1

θ1 = π/2 – θ2  ,cosθ1=cos(π/2 – θ2) = sinθ2

θ1 = -π/2 + θ2   ,cosθ1=cos(-π/2 + θ2) = sinθ2

至于怎么计算的,这个非常非常基础我就不讲了,套三角公式就ok:

https://baike.baidu.com/item/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0%E5%85%AC%E5%BC%8F

那么sinTH就是我们日思夜想的NdotH啦!!!

ok,既然拿到了NdotH,那么接下来做的就是对这个值进行一些处理

可以看到代码里返回的是dirAtten * pow(sinTH, exponent) * scale

做指数处理这个不用再解释了,跟昨天的效果是一样的,假设现在只返回一个pow(sinTH, exponent)(我使用的exponent参数值为20)会是什么样子呢?之后我们才能知道该怎么改进

嘻嘻嘻,是不是已经有点感觉了,调节exponent的值,当取到250的时候得到的效果是这样的:

这个值只是调整范围的,然后我们还需要调整强度对吧,所以再乘个scale: pow(sinTH, exponent) * scale

来看看效果:

最后发现跟常用的公式还差了一个值float dirAtten = smoothstep(-1.0, 0.0, dotTH);

先来了解了解这个值的算法,就也许可能大概知道为什么要再乘这个值了。dotTH的范围为[-1,1],当dotTH大于0的时候dirAtten值为1也就是原来的值没有影响,所以这个值针对的是dotTH处于[-1,0]的时候,也就是θ2∈[π/2,π],也就是前面我们没画的那种情况:

smoothstep的用法我就不再多说了,之前的文章写的很清楚了,简单来说dirAtten是通过dotTH构造出来的一个衰减系数,目的是让H远离T(角度超过九十度)的时候开始衰减高光。虽然为什么要让这部分的高光衰减,我暂时还没想通,不过既然原模型的有就加上吧(不过需要注意这只是一个衰减系数而已,不是核心的内容,我觉核心是计算出NdotH)

然后目前我们得到的效果就已经不错了:

但可以看出来太整齐了,太整齐了自然而言想到加张Noise图来让这些发丝错开!

看一下做这个偏移操作的函数吧:

float3 ShiftT(float3 T, float3 N, float shift)
{
		return normalize(T + shift * N);
}

ok,这里的T依旧是副切线哦,N就是这个片元的法线信息,shift就是从Noise图里采样得到随机值啦。这个其实很好理解,因为不是每根发丝都整整齐齐的,副切线也需要变得不太一样,这就是为什么要做这个的操作的原因,至于这个操作具体是怎么让T改变的,大家稍微想象一下就好了

下面直接上最终的效果图和参数(Noise图质量不高,将就看看,效果对就好啦,嘻嘻):

ok,头发的高光就完成啦,放到我的人物上是这样的效果(左:无高光,中:+次高光,右:+主高光)):

 

 

 

 

 

虽然参数啥的还有图调的还不够好看,不过效果是达到了,ok,今天的内容就先说到这,这几天肝了太多篇文章了,需要休养休养了,明天再继续加油冲!


Mavis , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:卡通渲染之头发高光(anisotropy)的实现(URP)
喜欢 (7)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址