Kajiya模型

Kajiya-Kay 是一种专门用于渲染头发的各向异性光照模型。它不再依赖传统 Lambert/Phong 中的“单点法线”,而是把发丝看作细长的圆柱体,用切线方向作为高光的各向异性主轴,从而复现头发沿发丝方向拉伸的亮斑。

本实现支持:切线空间法线贴图、双层各向异性高光、基于粗糙度的 IBL 环境反射,以及 ACES 色调映射。

头发截面高光示意

为什么要用 Kajiya 模型?

1. 表面法线的“不确定性”

几何类型 法线特点
平整表面 一个像素对应一个明确的法线
极细圆柱(头发) 一个像素可能覆盖整根发丝的半圆截面,法线在截面上连续变化

传统 Lambert 模型中,漫反射强度取决于光方向 L 与法线 N 的夹角。头发太细时,单像素内的法线并不唯一,所以 Lambert 模型不能直接套用,需要改进。

2. 视觉直觉:高光是“横”着的

竖着拿一根头发,亮斑不是一个点,而是一个横向的亮环。物理上,光线打在圆柱体上时,反射最强的部分永远出现在垂直于发丝的方向。

因此,计算高光时要关心的是光线/视线与发丝的夹角,而不是单纯的法线点乘。

3. T · H 的含义

高光项中的 TdotH 即:

T · H = cos θ

其中 T 是发丝切线方向,H 是半角向量(光方向与视方向的中间方向)。

从三角恒等式看:

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