
本文使用的技术,其比较广为人知的应用是《王者荣耀》中妲己的尾巴。腾讯的大佬还专门出过文章讲解该技术。 本人是在参考了知乎的一位大佬的文章和github项目后做出了这样的效果。 知乎文章地址: https://zhuanlan.zhihu.com/p/57897827 该文章中也引用了 腾讯大佬的那篇文章。
腾讯大佬那篇文章看起来比较抽象,但看完后基本的原理是可以弄懂的,知乎这篇也比较抽象,但是大佬在 github 上有完成的项目,对着代码看时就非常容易理解了。
我们一开始的想法很简单,毛是从皮肤下面长出来的,我们想让一张皮长出毛,那么我们就沿法线挤出顶点,这样就相当于皮长出了毛。 但实际上,我们如果只是简单的在 顶点着色器中写上 那么所有的顶点都会沿发现外扩,最终的效果就是整个物体都会变大。而我们想要的长出一根毛,实际上应该是一个顶点沿发现外扩,而其周围一圈的顶点都不动,这样才能形成尖状的毛。
那么怎么办呢? 这里使用的技术原理与体渲染十分相似,我们把 从皮肤到毛发顶端先看成一整块体积,这里称作 毛发体:

但是实际的毛发应该张这样呀:

也就是说,我们的 毛发体 内部有些地方是镂空的,而这些 镂空的部分 我们也可以将其看作是 透明的 那么,我们做出来毛发体后,通过采样贴图的方式,把一些地方的 alpha 值设为 0,这样毛发就出来了。
那接下来的问题就是怎么做出这个 毛发体?上面我们已经知道 如果只是对顶点做一个发现外扩 那么最终得到的面片它还是一个面片 不可能变成一个形体。 而在体渲染中一个重要概念就是 分片!我们所有的顶点沿法线外扩后得到的是一个面片,那么很多个外扩了不同距离得到的面片组合起来不就近似于一个形体了吗?

一系列面片曾紧密的组合在一起 就近似组成了一个形体。
那么接下来的问题就是如何产生这一系列面片?如果只是一个顶点着色器,那法线外扩后仍是一个面片。那么多个顶点着色器呢?Unity 的 Shader 是可以有多个 Pass的,可以对一个物体进行多次渲染并将这些渲染结果组合在一起,也就是说我们可以在 Shader 中 使用多个 Pass,每个 Pass 的顶点着色器都来产生一层面片,最后所有 Pass 的结果组合成了 毛发体 这里,我使用了 20 个 Pass,这意味着 该物体渲染一次至少 20个 DrawCall,应该有办法可以去减少 Pass 数的,这里本人还没有研究。 因为 这 20个 Pass 里面要做的事情都一样,如果每个 Pass 都写一遍那可就太累了,所以我们创建一个 cginc 文件,把要做的事情写到这里面,然后在 Pass 里面直接 include 这个文件就行了。

代码:


这里的 就是我们毛发的长度,而 是一个分层控制量,我们这 20层 分片是 范围,那么 就是 0~1 范围。如果这 20个分片是均匀分别的,那么 就是 1/20=0.05 的递增
所以在这 20个 Pass 中,我们调用 vert_fur 方法,传入的 就是 0.05 递增的

上图就是我们 20个分片组成的 毛发体
由于本人还没达到打通建模到渲染的水平,所以这里用的 模型 和 纹理 是用的 YOung 大佬项目中的模型和纹理 主纹理(漫反射纹理):

接下来我们就是通过采样的方式,来对每个片层相应位置的透明度进行设置了。 毛发的透明度大致有两个特点:
毛发越往上越尖
越尖的地方越透明,也就是越往上毛发越透明

毛发越往上越尖 是因为越往上 全透明的区域越大,即 alpha = 0 的部分越大 而毛发越往上越透明则是越往上 alpha 值越小
我们可以先把毛发看作一个柱体:

我们可以制作出一个 worley noise 纹理(沃利噪声):

实际上并不是一个标准的沃利噪声(但用沃利噪声应该也差不太远) 在这张图上,一个个晶胞就像我们的毛发。但这图上的晶胞很少,没关系,我们可以调整贴图的 tilling,这样这个纹理就包含很多晶胞了,也就可以表示很多毛发了。我们观察包含毛发的这个柱体,越往上 完全透明的部分越多,而越往上 是越大的,所以我们可以先采样这个噪声图 ,然后 ,这样 采样到纹理晶胞中心的点 就是毛发的中心 采样值比较大,进行 step 时 一般返回为 1,而越往边缘 则 越可能返回 0,且随着 层数的增加 即随着毛发向上 返回 0 的可能性越大,这样就形成了 下面粗 上面尖的形状:

但是 是 alpha 和 fur_offset 的线性梯度关系,形成的毛发太尖了,不够圆润,所以我们这里取 :

代码:

这里我们给 frag_fur 也加了 fur_offset 参数,所以 .shader 里也得加上:

现在 毛发 越往上越尖解决了,还剩下越往上越透明,这里让 alpha 乘上个 就行了

代码:

之后我们引入一个 AO 项,即环境光遮蔽项。因为光线在到达毛发根部的过程中 大部分都被毛发上面的部分给遮挡吸收了,所以毛发越往根部会越暗,那正好还是根据 fur_offset 越往上越大的特点,毛发的 AO 项可以与 进行关联,但是这样如果线性关联的话 效果不太好,也不太好控制效果。我希望 AO 项是 0~1 的范围, 也是 0~1 范围,且 0~1 范围的变量 他的任意非负数次幂 仍是 0~1 范围,那么就可以得到 AO 项了 :

代码:



剩下的就是 BRDF 光照着色了,注意 毛发的话 BRDF 中的 分布函数 应该用 各项异性的,BRDF 的具体实现这篇就不做介绍了,有时间再做一篇。其实BRDF主要是对反射光做解释,但普通毛发高光没那么明显,人类对毛发的高光印象也比较少,所以就算整个冯模型看着问题也不大。