Unity Shader - 毛发渲染,飘逸的毛发
本文介绍了一种在 Unity 中实现真实感动物毛发的技术。通过多层渲染、顶点偏移、噪音贴图和环境光照处理等手段,使得渲染出的毛发层次丰富且自然。此外,还介绍了如何使用 C# 代码控制毛发的动态效果,实现缓慢飘动的视觉表现。

上图:在 Unity 中实际渲染出的毛发效果预览。
渲染原理
整体思路可以分为三步:
第一步:分层 Layer,渲染不同长度的毛发
- 把毛发拆成多层(Layer)渲染,不同层表示不同长度的毛发;层越多,细节越丰富。
- 因为毛发有长短,在上层没有毛发的部分将 alpha 设为 0,不显示。
- 多个 Layer 可以使用 Shader 的多个 Pass 进行绘制:每一个 Pass 即表示一层。
- 当渲染每一层时,使用法线把顶点位置挤出模型表面,模拟毛发竖起的体积效果。
第二步:使用噪声贴图随机毛发分布与长度
- 根据噪音贴图,随机控制每个位置毛发的显示与长度,通过 alpha 变化实现。
- 不同层的毛发,其 UV 位置偏移也应该不同,这样才显得更自然;
否则 UV 一直不变,毛发方向看起来就像刺猬一样整齐。 - 毛发在不同层的偏移,也可以受到外力的影响,比如风等,所以可以额外设置一个偏移量。
第三步:处理光照,设置颜色
- 根据环境光、反射、高光、漫反射等,处理毛发的光照。
- 可以为毛根、毛尖、边缘光等设计不同的颜色,用以表现毛发的层次感和柔软度。
实现步骤
1. 多 Pass 渲染(按层渲染毛发)
对于皮毛来说,每一层的渲染方式是一致的,所以可以把公共逻辑提炼到一个 .cginc 文件中,在每个 Pass 中引用,避免重复代码。
不同的层次需要传入参数控制:
使用 (1 / 总层数) 作为每一层的长度步进,配合 FURSTEP 来控制当前层对应的“毛发深度”。
// 表面皮肤渲染
Pass
{
CGPROGRAM
#pragma vertex vert_surface // 顶点着色器,对应 FurHelper.cginc 中的方法
#pragma fragment frag_surface // 片元着色器,对应 FurHelper.cginc 中的方法
#define FURSTEP 0.0
#include "FurHelper.cginc" // 引用的 shader 辅助文件
ENDCG
}
// 毛发渲染
Pass
{
CGPROGRAM
#pragma vertex vert_base // 顶点着色器
#pragma fragment frag_base // 片元着色器
#define FURSTEP 0.05 // 当前这一层的皮毛深度
#include "FurHelper.cginc"
ENDCG
}
实际使用中可以通过宏或循环自动生成多层 Pass,这里只是概念示例。
2. 顶点外扩:实现多层次偏移
思路:在顶点的法线方向,向外扩展一定的距离,所以在顶点着色器中处理。
v2f vert (a2v v)
{
v2f o;
// 顶点沿法线方向外扩,形成毛发层
float3 offsetVertex = v.vertex.xyz + v.normal * _LayerOffset * _FurLength;
// 顶点受力偏移(例如重力或风的方向偏移,让毛发向下垂)
offsetVertex += mul(unity_WorldToObject, _FurOffset);
// 后续再把 offsetVertex 写回 o 进行常规的 MVP 变换
// 这里省略了完整的变换代码,仅展示核心思路
// ...
return o;
}
- _LayerOffset:当前 Shell 所处的层偏移(0~1)。
- _FurLength:毛发最大长度参数,用来限制最外层的扩展距离。
- _FurOffset:受力偏移(例如重力、风向),使毛发整体有一个方向上的偏移,看起来更自然。
3. 使用噪音贴图控制透明度
思路:利用第二套 UV(例如 uv2)采样噪音贴图,根据噪音值的随机性控制毛发透明度。
fixed3 noise = tex2D(_FurTex, i.uv.zw).rgb;
fixed alpha = saturate(noise - (_LayerOffset * _LayerOffset + _LayerOffset * _FurDensity));
- _FurTex:噪声/毛发分布纹理。
- i.uv.zw:通常为 uv2,专门用于采样噪声纹理。
- _FurDensity:毛发密集度参数,影响透明度计算,让毛发看起来更稀疏或更浓密。
不同层可以在 UV 上增加轻微偏移,避免各层噪声完全重合,提升自然感。
4. 参数提炼与优化
可以把常用参数提炼出来,方便美术和程序在 Inspector 中调节:
- 毛发长度参数:限制最外层的扩展长度(_FurLength)。
- 毛发层数:Shell 的数量(LayerCount)。
- 颜色参数:
- 整体毛发颜色(_Color / FurColor)
- 根部颜色(_RootColor / FurRootColor)
- 边缘光颜色(_RimColor / FurRimColor)
- 高光颜色(_Specular / FurSpecularColor)
- 毛发密集参数:影响噪声贴图采样、控制 UV 的 Tiling(_FurDensity)。
- 偏移参数:例如长毛宠物的毛会自然下垂,可用 FurForce、FurTenacity 等参数控制。
5. 使用 C# 控制毛发层与动态移动
为了实现更飘逸的动态效果,可以让 Shader 专注于“单层渲染”,而用 C# 来生成多层 Shell,并在 Update 中用 Lerp 让它们缓慢跟随主体移动。
思路:
- Shader 负责渲染“一层毛发”。
- C# 代码克隆多个带有同一 Shader 的 GameObject,根据层索引设置不同的 _LayerOffset 和 _FurOffset。
- 在 Update 中通过 Lerp 控制每一层渐进跟随目标位置和旋转,实现柔和的飘动感。
生成 Shell:克隆对象 & 赋值材质
// 生成:克隆目标,赋值材质与 shader
void CreateShell()
{
CheckParent();
layers = new GameObject[LayerCount];
float furOffset = 1.0f / LayerCount;
for (int i = 0; i < LayerCount; i++)
{
// 复制渲染的原模型
GameObject layer = Instantiate(Target.gameObject,
Target.transform.position,
Target.transform.rotation);
layer.transform.parent = _parent;
// 这里只用一张材质
layer.GetComponent<Renderer>().sharedMaterials = new Material[1];
layer.GetComponent<Renderer>().sharedMaterial = ResetSharedMaterials(i, furOffset);
layers[i] = layer;
}
}
为每一层设置 Shader 参数
// Shader 与毛发参数赋值
Material ResetSharedMaterials(int index, float furOffset)
{
Material special = new Material(ShellShader);
if (special != null)
{
special.SetTexture("_MainTex", Target.sharedMaterial.GetTexture("_MainTex"));
special.SetColor("_Color", FurColor);
special.SetColor("_RootColor", FurRootColor);
special.SetColor("_RimColor", FurRimColor);
special.SetColor("_Specular", FurSpecularColor);
special.SetFloat("_Shininess", Shininess);
special.SetFloat("_RimPower", RimPower);
special.SetFloat("_FurShadow", FurShadow);
special.SetTexture("_FurTex", FurPattern);
special.SetFloat("_FurLength", FurLength);
special.SetFloat("_FurDensity", FurDensity);
special.SetFloat("_FurThinness",FurThinness);
// 不同 Shell 的层偏移参数
special.SetFloat("_LayerOffset", index * furOffset);
// 计算受力、层数和“韧性”共同影响的 Shell 偏移
special.SetVector("_FurOffset",
FurForce * Mathf.Pow(index * furOffset, FurTenacity));
// 由于使用了半透明并写入深度,为防止被深度剔除,手动调整渲染队列
special.renderQueue = 3000 + index;
}
return special;
}
Shell 的缓慢跟随(飘逸移动)
// 在 Update 中控制每一层缓慢移动,营造“飘逸感”
void UpdateShellTrans()
{
if (layers == null || layers.Length == 0)
return;
for (int i = 0; i < layers.Length; i++)
{
// 位置和旋转 Lerp 到目标模型
Transform t = layers[i].gameObject.transform;
t.position = Vector3.Lerp(
t.position,
Target.transform.position,
lerpSpeed * Time.deltaTime * 20f
);
t.rotation = Quaternion.Lerp(
t.rotation,
Target.transform.rotation,
lerpSpeed * Time.deltaTime * 10f
);
}
}
通过以上逻辑,Shell 会在动画过程中略微“追随”主体,而不是 100% 绑死,从而看起来更柔和、更有动态感。
参考资料
- 真香预警!新版“峡谷第一美”妲己尾巴毛发制作分享
- https://github.com/Sorumi/UnityFurShader
- Fur Tool Unity Asset for 3D Character Modelling Artists | Coldface Interactive
- https://github.com/Acshy/FurShaderUnity