Kajiya-Kay模型是一种专门用于模拟头发、毛发等纤维状材质各向异性高光的光照模型。与传统的基于法线的高光计算不同,它采用切线方向作为计算基础,能够产生更符合真实毛发特性的条状高光效果。在Unity URP渲染管线中实现这一模型,需要深入理解其核心原理,并掌握将其整合到标准PBR工作流中的关键技术。
Kajiya-Kay模型的核心创新在于用切线向量(Tangent)或副切线(Bitangent)替代传统的法线向量进行高光计算。具体计算过程如下:
这种计算方式产生的不是圆形高光,而是沿着切线方向的条状高光,更符合毛发纤维的光学特性。
真实的毛发通常表现出两层高光效果:
这种特性模拟了Marschner模型描述的毛发散射行为,其中光线会在毛发内部发生多次散射,产生复杂的视觉效果。
为了增强高光的动态变化和真实感,Kajiya-Kay模型引入了切线偏移技术:
这种技术可以模拟毛发表面的微观不规则性,使高光效果更加自然。
Unity URP中的BRDF基于Cook-Torrance微表面模型,包含三个核心组件:
D项(法线分布函数):
F项(菲涅尔项):
G项(几何遮蔽项):
URP中的BRDF数据通常包含以下字段:
csharp复制struct BRDF {
float3 diffuse; // 漫反射颜色
float3 specular; // 高光颜色
float roughness; // 粗糙度
float perceptualRoughness; // 感知粗糙度
float fresnel; // 菲涅尔反射强度
}
将Kajiya-Kay模型整合到URP BRDF框架的第一步是正确处理切线空间:
在顶点着色器中计算TBN矩阵:
hlsl复制float3 T = input.tangent.xyz;
float3 N = input.normal;
float3 B = cross(N, T) * input.tangent.w;
将相关向量转换到切线空间:
hlsl复制lightDirTS = mul(TBN, lightDirWS);
viewDirTS = mul(TBN, viewDirWS);
用Kajiya-Kay的D项替换标准BRDF中的D项:
hlsl复制float D_KajiyaKay(float3 T, float3 H, float shininess) {
float TdotH = dot(T, H);
float sinTH = sqrt(1.0 - TdotH * TdotH);
return pow(sinTH, shininess);
}
保持F项和G项不变,或根据需要进行适当调整。
实现双层高光效果的关键步骤:
计算主高光:
hlsl复制float3 T_shifted1 = ShiftTangent(T, N, _ShiftAmount1);
float specular1 = D_KajiyaKay(T_shifted1, H, _Shininess1);
计算次高光:
hlsl复制float3 T_shifted2 = ShiftTangent(T, N, _ShiftAmount2);
float specular2 = D_KajiyaKay(T_shifted2, H, _Shininess2);
组合结果:
hlsl复制float3 specular = _SpecColor1 * specular1 + _SpecColor2 * specular2;
切线偏移函数:
hlsl复制float3 ShiftTangent(float3 T, float3 N, float shift) {
return normalize(T + N * shift);
}
Kajiya-Kay高光计算:
hlsl复制float D_KajiyaKay(float3 T, float3 H, float shininess) {
float TdotH = dot(T, H);
float sinTH = sqrt(1.0 - TdotH * TdotH);
return pow(sinTH, shininess);
}
光照计算函数:
hlsl复制void Lighting_KajiyaKay(
SurfaceData surface,
inout Light light,
inout BRDFData brdf,
inout float3 specular)
{
// 切线空间基础向量
float3 T = surface.tangent;
float3 B = cross(surface.normal, T) * surface.tangent.w;
float3 N = surface.normal;
// 计算半角向量
float3 viewDir = GetWorldSpaceNormalizeViewDir(surface.positionWS);
float3 H = normalize(light.direction + viewDir);
// 主高光计算
float3 T_shifted1 = ShiftTangent(T, N, _ShiftAmount1);
float specular1 = D_KajiyaKay(T_shifted1, H, _Shininess1);
// 次高光计算
float3 T_shifted2 = ShiftTangent(T, N, _ShiftAmount2);
float specular2 = D_KajiyaKay(T_shifted2, H, _Shininess2);
// 组合结果
specular = _SpecColor1 * specular1 + _SpecColor2 * specular2;
// 标准BRDF漫反射部分
brdf.diffuse = surface.albedo * (1.0 - _Metallic);
brdf.specular = lerp(0.04, surface.albedo, _Metallic);
brdf.roughness = _Roughness;
}
hlsl复制Properties {
_BaseColor ("Base Color", Color) = (1,1,1,1)
_BaseMap ("Base Map", 2D) = "white" {}
_SpecColor1 ("Primary Specular Color", Color) = (1,1,1,1)
_SpecColor2 ("Secondary Specular Color", Color) = (1,1,1,1)
_Shininess1 ("Primary Shininess", Range(1, 100)) = 50
_Shininess2 ("Secondary Shininess", Range(1, 100)) = 30
_ShiftAmount1 ("Primary Shift Amount", Range(-1, 1)) = 0.1
_ShiftAmount2 ("Secondary Shift Amount", Range(-1, 1)) = -0.1
_Roughness ("Roughness", Range(0, 1)) = 0.5
_Metallic ("Metallic", Range(0, 1)) = 0
_AnisoNoise ("Anisotropy Noise", 2D) = "white" {}
}
使用URP内置函数提升稳定性:
hlsl复制float3 H = SafeNormalize(light.direction + viewDir);
NormalizeNormalPerPixel(surface.normal);
正确生成副切线:
hlsl复制float3 B = cross(N, T) * input.tangent.w;
注意tangent.w分量用于处理UV方向
合理设置参数范围:
_ShiftAmount1/2:
_Shininess1/2:
_SpecColor1/2:
可能原因:
解决方案:
可能原因:
解决方案:
可能原因:
解决方案:
通过脚本控制参数实现动态效果:
csharp复制void Update() {
float shift = Mathf.Sin(Time.time) * 0.1f;
material.SetFloat("_ShiftAmount1", shift);
}
修改光照计算函数以支持多光源:
hlsl复制for (int i = 0; i < GetAdditionalLightsCount(); i++) {
Light light = GetAdditionalLight(i, surfaceData.positionWS);
// 对每个光源应用Kajiya-Kay计算
}
将Kajiya-Kay与更复杂的头发着色模型(如Marschner)结合:
在移动平台:
在高端平台:
通用优化:
长直发:
卷发:
短毛:
长毛:
丝绸:
羊毛:
调试视图:
参数调节:
性能分析:
移动平台:
桌面平台:
主机平台:
在实际项目中使用Kajiya-Kay模型时,有几个关键点值得注意:
一个实用的技巧是为常用材质创建预设参数,可以大大提高工作效率。另外,建议在项目早期就确定好毛发渲染的风格方向,避免后期大规模调整。