2 min to read
毛发shader
单PASS的shell模型毛发shader
目标:
单pass下用shell实现毛发效果。 可调节层数,长度等信息。 顶点运动使毛发配合物体运动。
制作:
1. 理解shell模型: 大致就是单根毛发由多层结构组成,当层数足够多和近的时候看起来就像毛发了。 毛发向法线方向生长,用alpha值(用噪点图来代替)来对每层大小进行递减。
2. 单层Shell实现:
第一步:先实现一个眼法线方向进行顶点外扩。
output.positionHCS = TransformObjectToHClip(float4(input.positionOS + input.normalOS * _SingleShellLen * layerNum ,1));
第二步:根据不同的shell级数来调节alpha值,越往外应该越细。
col.a = saturate(noise - _LayerNum);
col.a *= noise;
先将noise map采样作为了alpha。 然后根据不同的层数来对alpha进行递减来使尖端的毛看起来更细。但因为还没有实 现多层shell,所以还看不到实际效果。这里的LayerNum代表的是当前层数的offset,之后会通 过脚本传入。最后再乘上刚才的noise map (乘上毛发末端才不会连在一起)。 不关闭深度写入, 然后使用 OneMinusSrcAlpha 混合模式。
3.多层shell实现:
这是最重要的一步,多层的shell可以直接表现出毛发的效果,但因为要在单pass中达 到效果,所以用一个脚本来进行实现。 第一步:对于shader进行简单的设置就可以在材质面板看到开启gpu instancing的选项 ,不过现在还不够。用GPU instancing是以免之后开启脚本后开销太大。
#pragma multi_compile_instancing
目前大致思路是用脚本生成多个object然后对于不同的object传入不同的_LayerNum 参数,以此来达到多层shell的实现。(并把生成的object变为初始object的子物体) 第二步:将模型变为prefab,用shellLevel来控制层数。生成与层数-1相同的object (prefab)。
public Transform prefab;
public int shellLevel = 10;
void Start()
{ for (int i = 0; i < shellLevel; i++)
{ Transform level = Instantiate(prefab);
level.localPosition = transform.position;
level.SetParent(transform); } }
MaterialPropertyBlock propertyBlock = new MaterialPropertyBlock();
propertyBlock.SetFloat("_LayerNum", 0.1f * i * 0.02f);
level.GetComponent().SetPropertyBlock(propertyBlock);
同时每一个新生成的object传入不同的LayerNum参数,这里先根据i值来个简单的递 增。一个夸张的大致效果如下:
第三步:在shader中完善gpu instancing。 使用unity的宏将需要改变的参数先存入一个buffer。
UNITY_INSTANCING_BUFFER_START(InstanceProperties)
UNITY_DEFINE_INSTANCED_PROP(float, _LayerNum)
UNITY_INSTANCING_BUFFER_END(InstanceProperties)
并且在之后的代码中用下面这个宏来使用_LayerNum属性。
UNITY_ACCESS_INSTANCED_PROP(InstanceProperties,_LayerNum)
将之前的SingleShellLen参数也做相同的处理,方便在脚本中一起控制。 调节脚本让其可以实时修改各种参数(目前还不完善): 第四步:完善shader中的毛发基本效果。
col.a = (noise*2-(step *step +step*5)) * _FurDensity;
col.a *= noise;
在最后一步乘上noise map之前做一个拟合函数同时用一个新的参数来控制毛发的浓 密程度。也放进脚本一起修改[3]。
第四步:完善脚本。 之前的脚本实时更改有些问题(同时增加和减少层数会造成渲染顺序错误),于是改 成需要按键来进行生成。利用custom editor,给脚本增加两个按钮,一个用来清除原毛发,一 个用来生成新的毛发。
4.灯光:
毛发的基本效果达成了,再来为毛发配上合适的光照效果。大致分为2部分:环境光, sss效果。 第一步:SSS效果。在背面用类似菲涅尔产生一个简单sss效果。
float3 backLightDir = N * _BackSSDistortion + L;
float backSSS = max(0,min(1,1-dot(V,backLightDir)));
float3 sssResult = saturate(backSSS * _SSSIntensity);
第二步:环境光。环境光使用SH:
第三步:环境光遮蔽和阴影。设置一个阴影颜色,然后用layerOffset进行线性差值,使毛发根部 更暗,并加上参数设置[3]。同时对环境光进行遮蔽。对所有光照结果啊乘dot(N,L)
baseColor.rgb = lerp(_ShdowColor.rgb * baseColor.rgb,baseColor.rgb,layerOffset * _ShadowRange);
。。。
float3 ambient_SH = SampleSH(float4(N, 1));
half Occlusion =pow(layerOffset,2.2);
Occlusion +=0.04 ;
half3 SH_result = lerp (shadowColor*ambient_SH,ambient_SH,Occlusion * shadowRange) ;
5.毛发运动:
第一步:受重力影响。通过一个受layerOffset影响的曲线来对毛发进行偏移,越尖端受 重力影响越大。
float3 GetGravity(float layerOffset) {
return pow(layerOffset,3) * mul(unity_ObjectToWorld,_GravityDirection) * _GravityForce;
}
第二步:实现物体位移毛发也跟着位移。在shader中申明一个参数_MovementOffset, 然后在脚本中实时对这个参数进行更新。大致原理就是在物体运动时,对偏尖端的顶点 (pow(layerOffset,3)可实现[3])进行一个运动反方向的偏移,来实现一个毛发的运动效果。
float3 GetMovement(float layerOffset) { r
eturn pow(layerOffset,3) * mul(unity_ObjectToWorld,_MovementOffset);
}
现在shader中对刚才用来实现重力效果的代码进行修改,把固定的重力方向改为由脚 本传参的_MovementOffset。 在脚本中需要获得当前物体的运动方向,然后根据运动方向来决定_MovementOffset 并实时传给材质。 给父物体添加一个刚体,通过刚体来得到运动方向。(脚本中有一个简单的运动代码, 方便展示) parentRb.velocity = new Vector3(0.5f * Mathf.Sin(Time.time * 0.5f * Mathf.PI), 0.5f * Mathf.Sin(Time.time * 0.5f * Mathf.PI), 0); furOffset = parentRb.velocity;
再在脚本中添加可以翻转xyz轴的选项(方便调整在运动时毛的方向)
6.完善: 将材质中的光照相关参数加入脚本,同时将除了 shell层数参数以外的参数改变成在脚 本中实时更新。 脚本截图:
可以看到大部分参数都是使用这个脚本来进行调节。 材质中只需要放好相关贴图:
7.总结
如果在脚本中实时更新毛发的层数(实时删除和生成新的子物体),会造成渲染顺序上 的一些错误,非实时限制毛发层数的修改就解决了这个问题。
参考: 1. An introduction to shell based fur technique https://gim.studio/an-introduction-to-shell-based-fur-technique/
- Fur effects: https://www.xbdev.net/directx3dx/specialX/Fur/index.php 3.王者荣耀毛发制作:https://mp.weixin.qq.com/s/aIWMEO5Qa2gNn2yCmnHbOg 4.毛尾巴:https://zhuanlan.zhihu.com/p/59483593 5.GPU实例:https://mp.weixin.qq.com/s/qDPfrn2Vtw4qLUpiOBCa8g 6.毛皮材质:https://zhuanlan.zhihu.com/p/57897827