关于作者:梁家斌,腾讯互动娱乐天美工作室群高级游戏美术师。
之前有很多人来询问新版妲己宝宝
毛茸茸的尾巴 做法,
先谢谢大家对这个毛发效果的认可,
我在这里就简单的分享一下,
毛发的实现思路和制作方法。
毛绒材质在生活中出现的频率非常高,
但是在各种游戏中,
我们却很少看到这种材质效果的良好表现,
原因在于它们的制作与渲染成本都太高了。
所以, 实时毛发渲染
是业内最为期待的次世代特效之一。
我们先看看这些材质的特征:
他们有着柔软的丰富的外轮廓、
柔和的光影、温暖,
这种材质表现一直是女孩子所喜爱的。
因为毛发复杂性与庞大的计算量
(每根毛发产生的阴影遮挡),
所以在毛发渲染方面,
基本一直都是离线渲染的专利。
直到最近,
GPU处理能力才达到实时计算毛发的标准,
当然那也是针对PC平台而言,
我们手机暂时还望尘莫及。
我们一般游戏的毛发渲染,
都是将毛发纹理放到模型面片上面,
用AlphaBlend或者AlphaTest剔除镂空区。
但这两者也都无法尽善尽美:
-AlphaBlend没有深度会有模型穿插问题。
-AlphaTest有顶点深度但边缘锯齿感严重,
需要在SSAA下面才不会有明显的锯齿。
-两者结合使用效果比较好,
但会带来更高的性能消耗。
所以至今为止,
游戏上面都很难做到满意的毛发表现,
尤其是手游,
往往我们只能从设计层面去规避这些问题。
要制作一个东西,
我们必须抓住它的特点。
那么毛发有哪些特征呢?
我稍微归纳了一下:
通透性-次表面散射:
毛发是非常的细小的,
光线很容易就可以穿透它的表面,
尤其是浅色毛发,
颜色越深吸收光线的能力越强,
所以深色发毛的透光性相对较低。
这也是为啥很多女孩子
比较喜欢染发、烫发的原因之一。
因为浅色比黑色的毛发,
在逆光下的效果要漂亮很多。
烫发可以使头发蓬松,
让更多的光线穿透与反弹,
这样整体的层次感会变得很丰富。
复杂的遮蔽性:
毛发又非常的密集,
毛发相互之间的阴影遮挡造成毛发。
密集的地方光线也很难继续渗透进去,
就形成了发根到发梢的阴影渐变。
特殊的高光-各项异性高光:
头发的表面并不是光滑的,
上面有一层被称之为毛鳞片的结构,
产生的高光散射性很高,
很多人造毛很难做到这一点。
这也是为啥很多廉价的假发的质感
让人一眼就看出来不真实的原因。
上面我们说了头发,
是排列在一起的一个个细小圆柱体。
很多人不理解为什么
我们要把头发的高光叫做各项异性高光,
我用一张图简单说明下。
我们这里提到的所谓各向异性高光,
其实是我们没办法真的将头发
用一个个圆柱体来渲染,
而使用一种基于观察近似的算法模拟结果。
而动物的短毛皮比人类头发更加复杂,
可以大致分为两类,
一种是表层的长毛(被毛),
另一种是内层的绒毛(更加细小、弯曲)。
丝绸同理但更加复杂,
不同的丝本身质感就不一样,
加上不同针织结构,
如纱、绮、绢、锦、罗、绸、缎等10多类,
造成丝绸效果最终质感有很大的差异性,
有兴趣可以深入了解不同丝绸的微观结构。
说了这么多。
在手机游戏里怎么实现这些效果呢?
用顶点细分来制作不现实,
这么大量的毛发,
又是移动端平台,
机器的计算性能达不到要求。
而用大量面片插片的方式,
会占用大量3D美术的人力成本,
尽管效果是很好,
相对效果来说也不太划算。
最后选择了美术制作上最简单的
多pass渲染的方案。
因为这套方案在美术资源制作上,
基本上和普通角色资源没有差别,
唯一的限制就是skin多边形数量上的限制。
而且这套方案在PC端游上已经比较成熟,
比如《剑灵》。
多pass的方法局限性也很大,
不过我们可以拆分开来研究和优化。
layer实现方式
根据模型使用层 (layer) 来渲染毛发长度,
在 Unity Shader 中,
每一个 Pass 即表示一层。
当渲染每一层时,
使用法线将顶点位置挤出模型表面 。
Pass及使用的层数越多渲染效果越好,
当然开销也越大。
这种做法如果想要很好的表现,
就需要大量的pass。
如何用少量的pass实现更好的效果,
后面我们再一步步解决。
然后将Noise贴图根据layer做衰减,
来当做alpha值。
一定加入基于layer层高度的UV偏移。
• 没有UV偏移效果的毛怎么看都会像刺猬。
• 记得对毛发做UV偏移的时候,
Diffuse贴图的UV也要跟着一起计算哦。
float2 uvoffset= _UVoffset.xy * FUR_OFFSET;
uvoffset *= 0.1 ; //尺寸太大不好调整 缩小精度。
float2 uv1= TRANSFORM_TEX(v.texcoord.xy, _MainTex ) + uvoffset * (float2(1,1)/_SubTexUV.xy);
float2 uv2= TRANSFORM_TEX(v.texcoord.xy, _MainTex )*_SubTexUV.xy + uvoffset;
o.uv = float4(uv1,uv2);
half3 NoiseTex = tex2D(_SubTex, i.uv.zw).rgb;
half Noise = NoiseTex.r;
color.rgb = lerp(_Color,_BaseColor,FUR_OFFSET) ;
color.a = saturate(Noise-FUR_OFFSET) ;
return color;
也可以加入风力、重力等顶点控制项。
根据不同性能的机器来选择是否开启。
也可以将多层Noise
混合到一起来做一些不同的毛发,
将他们分别放到R、G、B不同的通道里,
可以减少贴图量。
缺点:
• 这样大家能看出来,
多pass的制作方式,
无法制作头发这样的长毛,
只能制作较短的毛发。
不过我们这次的目标也是制作短毛,
所以长毛与头发可以先抛开。
• 要有比较好的效果,
就需要非常多的pass来进行计算,
这也是我们不希望的。
因为移动平台对大量Overdraw这样的
像素级处理是非常大的一笔开销。
layer的层数是越少效率越高的,
在低layer上得到更好效果是我们的目标。
我将控制半透明毛发的曲线做了一些优化,
勉强可以在低layer上面达到多layer的效果。
优化Layer数量
一般的做法:
优化后:同时加入了可控的变量。
灯光
到现在为止,
在外形上基本接近了我们想要的毛发效果。
我们还需要将毛发的渲染特征也加上去。
这里用3个部分来实现:
环境光、轮廓光、太阳光
环境光:
环境光可以是一个单色,
也可以是一个微弱的顶底渐变,
或者球协光照(提取Hdir贴图低频数据)。
这里以简单的顶底颜色为例:
float3 normal = normalize(mul(UNITY_MATRIX_MV, float4(v.normal,0)).xyz);
half3 SH = saturate(normal.y *0.25+0.35) ;
前面讲了毛发的特点之一
就是环境光遮蔽与自阴影,
缺少了环境光遮蔽的效果只能打20分。
环境光遮蔽形成的散射是带有颜色的,
会根据物体的颜色不同产生不同颜色。
我很懒,就没将颜色与环境光遮蔽
之间的关系公式写进去,
直接开放一个颜色手动设置反弹的颜色。
(也能节约一些计算不是~)
half Occlusion =FUR_OFFSET*FUR_OFFSET; //伽马转线性最精简版
Occlusion +=0.04 ;
half3 SHL = lerp (_OcclusionColor*SH,SH,Occlusion) ;
但要记住固有色越浅,
反弹光就越强,
Visibility的影响就越弱;
反之颜色越深,
反弹光就越弱,
Visibility的影响就越强;
简单的说就是:
物体的颜色越浅,AO颜色越浅;
反之颜色越深,AO颜色越深。
轮廓光:
轮廓光其实也是环境光的一部分。
这里单独给轮廓光计算,
也只是弥补环境反光的不足,
同时加一些可控项,
也可以调出一些特殊的不一样的效果。
同时也和上面的一样,
物体的颜色越浅,
轮廓光穿透率越强;
反之颜色越深,
轮廓光穿透率越弱。
这里也一定要加入环境光遮蔽的遮挡,
因为毛发的透光性,
边缘稀疏的部分光线穿透率更高。
同时模拟了在环境光下次表面散射效果。
差异性在低多边形下会更加明显。
half Occlusion =FUR_OFFSET*FUR_OFFSET; //伽马转线性最精简版
Occlusion +=0.04 ;
half Fresnel = 1-max(0,dot(N,V));//pow (1-max(0,dot(N,V)),2.2);
half RimLight =Fresnel * Occlusion; //AO的深度剔除 很重要
RimLight *=RimLight; //fresnel~pow简化版
RimLight *=_FresnelLV *SH; //加上环境光因数
SHL +=RimLight;//与环境光结合
将Occlusion的计算
放到RimLight平方的前面,
是因为模型的多边形数量低,
轮廓会比较明显。
将Occlusion4次方,
能更好的减弱低多边形的影响。
因为我们用于毛发材质的模型面数,
是非常非常低的,
如果模型面数相对高一些的模型,
可以放到后面。
太阳光:
我们平常说的太阳光,
其实可以把它看成是平行光。
太阳其实是个点光源,
不过因为它过于庞大,
光线到地球上面的夹角非常非常小,
再到我们的可视物体的时候,
就完全可以忽略掉了。
我们就取最简单的公式。
half3 lightDir = -_SGameShadowParams.xyz; //外部传入的灯光方向
half NoL =dot(lightDir,normal);
half DirLight =NoL * _FurDirLightExposure*_DirLightColor;
普通光照模型这样计算没有问题,
但完全没有毛发的特性:
缺少太阳光在边缘的穿透性,
也没有逆光下的毛发次表面散射效果。
缺少每根毛产生的复杂的阴影表现。
用一个最简单的拟合就可以得到这个效果。
主要利用了NdotL(-1~1)的特性。
_LightFilter("平行光毛发穿透", Range(-0.5,0.5)) = 0.0
half3 lightDir = -_SGameShadowParams.xyz;
half NoL =dot(lightDir,normal);
half DirLight= saturate (NoL+_LightFilter+ FUR_OFFSET ) ;
DirLight *=_FurDirLightExposure*_DirLightColor;
最后将所有光照合并到一起:
• 为了节省性能,
所有灯光与颜色计算全部在顶点空间完成;
• 像素空间只用来计算贴图采样;
• 很多需要贴图一起计算颜色值的地方
都优化省略掉了,比较遗憾。
• 所有颜色计算都是在线性空间进行的
所以最后要转换到伽马空间,
这一步也可以去掉,
也可以再加上简单的tommping,
让画面颜色变得更好。
高光 - Anisotropic(各项异性)
前面讲毛发特性的时候,
对各项异性高光做了个简单的介绍。
学术上Anisotropic(各项异性)的解释:
• 某些材质上有一些微观上有方向的细丝,
这些细丝在宏观角度来看是不易察觉的,
典型的有光盘的背面或者是头发。
• strand based anisotropy是
对上述光照情形的一种建模。
(http://www.bluevoid.com/opengl/sig00/advanced00/notes/node159.html)
一些Anisotropic表现的例子:
各向异性制作的各种具体实现方式
我就不一一细说了:
float3 TShift(float3 tangent,float3 normal,float shift)
{
return normalize(tangent + shift * normal);
}
float StrandSpecular(fixed3 T,fixed3 V,fixed3 L,fixed exponent)
{
float3 H = normalize(L+V);
float dotTH = dot(T,H);
float sinTH = sqrt(1- dotTH * dotTH);
float dirAtten = smoothstep(-1,0,dotTH);
return dirAtten*pow(sinTH,exponent);
}
用抖动忒图来弥补头发细节。
头发比较特殊
观察头发的高光会发现,
其中一层高光是有颜色的,
另外一层高光是没有颜色的,
且两层高光的相互错开一点点。
我们根据公式在VS里计算出高光效果。
这时候会发现高光效果会很粗糙,
简直有点不堪入目。
一步一步来优化。
• 依然使用上面已经使用老套的方法,
用FUR_OFFSET做高光的遮蔽后,
因为VS渲染精度方面过低的问题缓解了,
但效果方面依然不是太好。
fixed SPec1 =StrandSpecular (T1,V,L,_specExp.x)*FUR_OFFSET;
• 因为毛束是一个个细小的单个圆柱体,
它的高光也并不是一个平面连续的表现。
我想了很多方法来弥补毛发体积与细节。
最终下面的结果相对来说,
得出的结果是目前得出的最好的。
大家猜一下——下面这张图是怎么得到的?
其实这就是我们目前的alpha当做色彩输出的结果。
• 越靠近透明的区域越黑,中间区域偏亮。
• 它刚好能达到我们高光缺少的细节部分的要求。
我就用它来当做单根毛发边缘体积的遮挡。
得到的结果很不错。
同时这个也替代了抖动贴图。
加入低频与高频2层高光后的效果更加自然。
fixed3 T1 = normalize(_specExp.z*normalWorld+binormalWorld);
fixed3 T2 = normalize(_specExp.w*normalWorld+binormalWorld);
fixed SPec1 =StrandSpecular (T1,V,L,_specExp.x) *FUR_OFFSET;
fixed SPec2 =StrandSpecular (T2,V,L,_specExp.y) *FUR_OFFSET;
o.Specular = SPec1*_SPColor1 + SPec2*_SPColor2;
高光部分我测试了很多方案,
目前效果只能说还能凑合着用,
因为都是在VS进行的计算,
效率上面应该还行。
第一次写分享,
罗里吧嗦的写了一大堆。
效果方面肯定还有非常大的优化空间。
欢迎大家一起讨论和指正。
一些个人遇到的问题:
• 毛发UV控制部分我加入了flowmap,
但是FlowTex的绘制太困难,
而且也不直观。
可以做个工具实时直观看到flowmap
在毛发上面的绘制效果。
甚至可以直接在unity绘制到模型顶点色
XY2个值上面控制毛发方向。
• 多模型重叠的时候还是有深度穿插。
分享一个16年的国外视频,
用了另一种方案来控制毛发长度与方向。
·END·