1
目录
这个系列主要分为三个部分:
1.头发的渲染
2.皮肤的渲染
3.眼睛的渲染
每个分为2个文章,第一篇讲基础理论,第二篇讲实际操作,最后的工程文件分别是Shader代码和Shader图表两种形式。
本次是写实头发的各向异性表达——Kajiya-Kay光照模型。
2
正文
1
观察
在游戏内,出于性能的考虑。引擎不会做到和DCC软件使用离线渲染器渲染出来一样的毛发效果,如Maya的XGen这种质感。这时候我们就有了替代方案,即:使用面片头发。这就涉及到了如何去表达头发的质感问题。
毛发的各异
螺纹不锈钢的各异
2
分析
我们先从美术角度分析一下头发:
我们可以看到头发上的高光和传统的球体的高光不一样,头发的高光更近似一个不规则的条状(如果放置在一个平面上)。
而且条状的上下似乎有一定的渐变规律,暗-亮-暗。金属器皿上的高光波纹近似。我们着重介绍头发的理论基础。
上图圈1的区域是亮的,圈2是暗色,这里我们从观察可以得出头发的高光形状与明暗的变化。如果观察的仔细的话,可以回忆一下,生活中,看到姑娘的头发,会跟随看的方向和光产生偏移,以及透光性(散射)。
3
总结
从美术角度总结一下:
1.需要用面片做出好看的头发模型。
2.头发想要好看必须要有一个这种形状的高光,并且有明暗过度。
3.能受到灯光的影响,表达出她的透气感。
我们再从程序的角度分析一下头发:
找案例,Render Doc截帧,Profile,Frame Debug ,review源码,一气呵成(哈哈,开个玩笑)
既然说到这里,我们就引入GDC2004的那片PDF的内容吧——Kajiya-Kay模型传送门:
http://amd-dev.wpengine.netdna-cdn.com/wordpress/media/2012/10/Scheuermann_HairRendering.pdf
4
Kajiya-Kay光照模型理解
定义:Kajiya-Kay模型是制作头发各项异性的核心部分,是指利用切线方向来计算高光强度。
什么是切线?
这里我们要回到模型本身的概念去了,我们都知道,模型是有法线(Normal)的,一般情况下垂直于当前平面的那根线,我们称之为法线。表示方法为N
当然,有正反之分,如果说你的模型进了引擎不显示或者不全,光照有问题,检查一下是不是法线反了。
WIKI内的定义:三维平面的法线是垂直于该平面的三维向量。曲面在某点P处的法线为垂直于该点切平面的向量。
如上图:
切线(Tangent)是垂直于法线的一条向量,由于垂直于法线的向量有无数条,所以切线最终规定为由UV坐标来决定朝向。我们记为T‘
副切线(Bitangent)有时也被叫作副法线(Binormal)。同时垂直于由法线与切线的向量,所以可以由法线与切线的叉积计算得出。记为B‘。
在Unity的源码中,有这样一段:
(注:worldBinormal就是我们要的副切线,这里写做了副法线)
可以看出利用cross(叉积)来得出向量,然后再乘上tangentSign得到最终确定方向的副切线。所以说副切线的方向主要是由tangentSign来决定的,而tangentSign是由v.tangent.w*
unity_WorldTransformParams.w计算得出的。
这里的法线切线副切线的基础内容,在知乎的Taecg的一片文章介绍的非常详细了。我这里只是拾人牙慧简单捋一遍。传送请走
:https://zhuanlan.zhihu.com/p/103546030
5
回归到公式理解
T,即为上文讲到的切线。但是这里是副切线,H是什么呢?H是光入射方向L和视口方向V的中间向量,通常也称之为半角向量(Half),半角向量被广泛用于各类光照模型如Blinn—Phong。
为什么会有这个H呢?我们来回想一下Blinn-Phong的光照模型和示意图:
Blinn-Phong和Ponng的区别就在于多了一个H
当视线正好与反射向量对齐时,半程向量就会与法线完美契合。所以当观察者视线越接近于原本反射光线的方向时,镜面高光就会越强。
获取半程向量的方法很简单,只需要将光线的方向向量和观察向量加到一起,并将结果归一化(Normalize)就可以了。
在引入半向量H之后,我们现在应该就不会再看到Phong光照中高光断层的情况了。下面两个图片展示的是两种方法在镜面光分量为0.5时的对比:
除此之外Blinn-Phong就没什么好说的了,Blinn-Phong与Phong唯一的区别就是,Blinn-Phong测量的是法线与半程向量之间的夹角,而Phong测量的是观察方向与反射向量间的夹角。
绕了一堆,不就是Blinn—Phong高光的计算公式么!
我们知道了这两个数值,回到公式,我们再看一下,是不是了然了?
哦!就是副切线和LightingDir+ViewDir的和,然后点积,然后平方,然后用1减去这个值后再开方!最后想要到这个效果我们再平方。
6
实践光照模型
我们用Graph复原一下逻辑,验证一下想法!
Graph逻辑图
这个高光模型我们给到一个球上观察,OK,基本的环状效果已经有了,剩下的就是加入comb map 来细化了。
观察
CombMap采样
写在后面:头发的光照模型我感觉更加近似一个经验值模型,是基于观察近似的算法而非基于理论模拟。我们可以在这个理论模型的基础上进行一些改进,让它更加符合我们想要的一个效果。原理虽然枯燥,但确实很重要。具体的改动,会在实操的第二章节介绍。
头发卡我卡得最难受的地方可能就是半透排序的方式了。思路是从模型下手加上双Pass解决。
7
完整内容效果
8
完整内容预习
以下是完整的Graph图表和Shader代码部分(手撸),有兴趣的朋友可以先看一下。具体工程下一篇讲完的时候放出来。
9
深入理解
上文钟我们知道了公式,回忆一下:
也用图表搞了一个简单的光照模型。
我对公式重新简单梳理:
上次有看官留言到:
这里讲一下为什么会选择副切线,因为头发的UV是从V方向铺开的。
10
运用
透明是一切混乱的开端。
这里读者必须要理解几个概念:
- 深度写入
- 颜色写入
- 深度测试
- 渲染队列
- 渲染排序
- 这里没法花很大的篇幅讲解,具体直接看乐乐入门书的第8章
- 只能简单的概括一下,对于半透的东西,渲染排序是很严谨的。透明物体的渲染一直是图形学方面比较蛋疼的地方。
- 对于透明物体的渲染,就不能像渲染不透明物体那样多快好省,因为透明物体不会写深度,也就是说透明物体之间的穿插关系是没有办法判断的,所以半透明的物体在渲染的时候一般都是采用从后向前的方法进行渲染。
- 由于透明物体多了,透明物体不写深度,那么透明物体之间就没有所谓的可以通过深度测试来剔除的优化,每个透明物体都会走像素阶段的渲染,会造成大量的Over Draw。这也就是粒子特效特别耗费性能的原因。
下面,我开了双Pass来解决渲染次序这个问题,即:
第一个Pass只渲染背面关闭深度
第二个Pass只渲染正面开启深度
由于Unity会顺序执行SubShader中的各个Pass,因此我们可以保证背面总是在正面被渲染之前渲染,从而可以保证正确的深度渲染关系。
对于不会代码的美术同学,没关系,贴心的ASE具备双Pass的功能(虽然最多只支持到双Pass,我觉得挺良心了)
Pass1:
Pass2:
下面,展示一下我用的模型和贴图:
mask:
Diffuse:
主要用来提Alpha的,虽然可以合到上边的Mask的A通道内,太懒,就没合。
Normal:
11
美术篇
先定义一个法线,转到世界空间,然后使用Mask的G通道与之相乘,要记得归一化。
然后将其与我们的世界空间下的副切线加在一起,同时,我也Public了一个float类型的值,给进去,是为了能够更好的偏移高光位置。可以自行脑补下高光上下偏移,最后记得归一化。
输入灯光方向+视口(观察)方向
将他们Dot(点积)后平方,一减后开方,到这一步发现,我们已经几乎完成了Kajiya-Kay光照模型的公式。
为了做出柔和过度的高光,观察一下下图,1.区域很亮,2.区域柔和。
新Public 四个值,并且使用Pow来约束他们范围强度,使用Mask的B通道乘进去,做出头发的黑白区域,然后将他们与颜色相乘后叠加(Add)在一起。
接下来,使用观察方向和世界法线做一次Pow,来控制高光两边的范围。
将其叠加在一起,使用一个常量控制强度。
颜色部分,我构建了一个类似lambert的lerp来插值两个颜色,为的是模拟受光面到暗面的过渡效果,即透气感。
到了提Alpha的时候了,顺便控制一下Alpha的强度。
我们将插值后的Color和高光区域叠加,与平行光颜色强度相乘。 让他受光照影响。
最后,我们和alpha组合,分别输出到pass1和pass2。
我们挂起FrameDebugger。
可以看一下他的渲染顺序,先渲染了Pass1,即剔除前面的。
再渲染了Pass2剔除了背面的。
程序部分我就不详细介绍了,重在思路。因为这次相比上一篇节点和程序的版本,有些地方有优化改动,Shader脚本还没来得及改动。如果需要脚本等到下一篇文章的时候再发出来吧。