Kajiya模型
Kajiya-Kay 是一种专门用于渲染头发的各向异性光照模型。它不再依赖传统 Lambert/Phong 中的“单点法线”,而是把发丝看作细长的圆柱体,用切线方向作为高光的各向异性主轴,从而复现头发沿发丝方向拉伸的亮斑。
本实现支持:切线空间法线贴图、双层各向异性高光、基于粗糙度的 IBL 环境反射,以及 ACES 色调映射。

为什么要用 Kajiya 模型?
1. 表面法线的“不确定性”
| 几何类型 | 法线特点 |
|---|---|
| 平整表面 | 一个像素对应一个明确的法线 |
| 极细圆柱(头发) | 一个像素可能覆盖整根发丝的半圆截面,法线在截面上连续变化 |
传统 Lambert 模型中,漫反射强度取决于光方向 L 与法线 N 的夹角。头发太细时,单像素内的法线并不唯一,所以 Lambert 模型不能直接套用,需要改进。
2. 视觉直觉:高光是“横”着的
竖着拿一根头发,亮斑不是一个点,而是一个横向的亮环。物理上,光线打在圆柱体上时,反射最强的部分永远出现在垂直于发丝的方向。
因此,计算高光时要关心的是光线/视线与发丝的夹角,而不是单纯的法线点乘。
3. T · H 的含义
高光项中的 TdotH 即:
T · H = cos θ
其中 T 是发丝切线方向,H 是半角向量(光方向与视方向的中间方向)。
T · H越接近 1:半角方向与发丝平行 → 反光弱;T · H越接近 0:半角方向与发丝垂直 → 反光最强。
从三角恒等式看:
sin²θ = 1 - cos²θ = 1 - (T · H)²
sin θ 越大,代表光线越“横着”扫过发丝,高光越强。

ACES 色调映射
inline float3 ACES_Tonemapping(float3 x)
{
float a = 2.51f;
float b = 0.03f;
float c = 2.43f;
float d = 0.59f;
float e = 0.14f;
float3 encode_color = saturate((x * (a * x + b)) / (x * (c * x + d) + e));
return encode_color;
}
顶点着色器
v2f vert(appdata v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
o.uv = v.texcoord;
o.pos_world = mul(unity_ObjectToWorld, v.vertex).xyz;
o.normal_world = normalize(UnityObjectToWorldNormal(v.normal));
o.tangent_world = normalize(UnityObjectToWorldDir(v.tangent.xyz));
o.binormal_world = normalize(cross(o.tangent_world, o.normal_world) * v.tangent.w);
TRANSFER_VERTEX_TO_FRAGMENT(o);
return o;
}
片段着色器
half4 frag(v2f i) : SV_Target
{
// ---------- 贴图信息 ----------
half4 albedo_color_gamma = tex2D(_BaseMap, i.uv);
half4 albedo_color = pow(albedo_color_gamma, 2.2) * _BaseColor;
half3 base_color = albedo_color.rgb;
half roughness = saturate(_RoughnessAdjust);
half3 normal_data = UnpackNormal(tex2D(_NormalMap, i.uv));
// ---------- 方向向量 ----------
half3 view_dir = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
half3 normal_dir = normalize(i.normal_world);
half3 tangent_dir = normalize(i.tangent_world);
half3 binormal_dir = normalize(i.binormal_world);
float3x3 TBN = float3x3(tangent_dir, binormal_dir, normal_dir);
normal_dir = normalize(mul(normal_data.xyz, TBN));
// ---------- 光源信息 ----------
half3 light_dir = normalize(_WorldSpaceLightPos0.xyz);
half atten = LIGHT_ATTENUATION(i);
// ---------- 直接光的漫反射 ----------
half diff_term = max(0.0, dot(normal_dir, light_dir));
half half_lambert = (diff_term + 1.0) * 0.5;
half3 common_diffuse = diff_term * _LightColor0.xyz * base_color * atten;
#ifdef _DIFFUSECHECK_ON
half3 direct_diffuse = base_color;
#else
half3 direct_diffuse = half3(0.0, 0.0, 0.0);
#endif
// ---------- 直接光的镜面反射 ----------
half2 uv_aniso = i.uv * _AnisoMap_ST + _AnisoMap_ST.zw;
half aniso_noise = tex2D(_AnisoMap, uv_aniso).r - 0.5;
half3 half_dir = normalize(light_dir + view_dir);
half NdotH = dot(normal_dir, half_dir);
half TdotH = dot(half_dir, tangent_dir);
half NdotV = max(0.0, dot(view_dir, normal_dir));
float aniso_atten = saturate(sqrt(max(0.0, half_lambert / NdotV))) * atten;
// 第一层高光
float3 spec_color1 = _SpecColor1.rgb + base_color;
float3 aniso_offset1 = normal_dir * (aniso_noise * _SpecNoise1 + _SpecOffset1);
float3 binormal_dir1 = normalize(binormal_dir + aniso_offset1);
float BdotH1 = dot(half_dir, binormal_dir1) / _SpecShininess1;
float3 spec_term1 = exp(-(TdotH * TdotH + BdotH1 * BdotH1) / (1.0 + NdotH));
float3 final_spec1 = spec_term1 * aniso_atten * spec_color1 * _LightColor0.xyz;
// 第二层高光
float3 spec_color2 = _SpecColor2.rgb + base_color;
float3 aniso_offset2 = normal_dir * (aniso_noise * _SpecNoise2 + _SpecOffset2);
float3 binormal_dir2 = normalize(binormal_dir + aniso_offset2);
float BdotH2 = dot(half_dir, binormal_dir2) / _SpecShininess2;
float3 spec_term2 = exp(-(TdotH * TdotH + BdotH2 * BdotH2) / (1.0 + NdotH));
float3 final_spec2 = spec_term2 * aniso_atten * spec_color2 * _LightColor0.xyz;
#ifdef _SPECCHECK_ON
half3 direct_specular = final_spec1 + final_spec2;
#else
half3 direct_specular = half3(0.0, 0.0, 0.0);
#endif
// ---------- 间接光的镜面反射(IBL) ----------
half3 reflect_dir = reflect(-view_dir, normal_dir);
roughness = roughness * (1.7 - 0.7 * roughness);
float mip_level = roughness * 6.0;
half4 color_cubemap = texCUBElod(_ENVMap, float4(reflect_dir, mip_level));
half3 env_color = DecodeHDR(color_cubemap, _ENVMap_HDR); // 确保在移动端能拿到 HDR 信息
#ifdef _IBLCHECK_ON
half3 env_specular = env_color * _Expose * half_lambert;
#else
half3 env_specular = half3(0.0, 0.0, 0.0);
#endif
// ---------- 最终合成 ----------
float3 final_color = direct_diffuse * 2.0 + env_specular + direct_specular;
final_color = ACES_Tonemapping(final_color);
final_color = pow(final_color, 1.0 / 2.2);
return float4(final_color, 1.0);
}
ENDCG