移动端人物渲染

面向移动端优化的Shader,基于金属/粗糙度工作流,结合 LUT 次表面散射(SSS)、球谐(SH)环境漫反射、立方体贴图(IBL)镜面反射与 ACES 色调映射。


主要特性


球谐环境光

float3 custom_sh(float3 normal_dir)
{
    float4 normalForSH = float4(normal_dir, 1.0);

    // L0 + L1
    half3 x;
    x.r = dot(custom_SHAr, normalForSH);
    x.g = dot(custom_SHAg, normalForSH);
    x.b = dot(custom_SHAb, normalForSH);

    // L2 前四项
    half3 x1, x2;
    half4 vB = normalForSH.xyzz * normalForSH.yzzx;
    x1.r = dot(custom_SHBr, vB);
    x1.g = dot(custom_SHBg, vB);
    x1.b = dot(custom_SHBb, vB);

    // L2 最后一项
    half vC = normalForSH.x * normalForSH.x - normalForSH.y * normalForSH.y;
    x2 = custom_SHC.rgb * vC;

    float3 sh = max(float3(0.0, 0.0, 0.0), (x + x1 + x2));
    sh = pow(sh, 1.0 / 2.2);
    return sh;
}

该函数计算三阶球谐环境光,等价于 Unity 内置的 ShadeSH9 简化版本。输入为世界空间法线,输出为环境漫反射颜色。


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;
}

ACES 拟合公式,把 HDR 光照结果压缩到 LDR 显示范围。


顶点着色器

v2f vert(appdata v)
{
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.uv  = v.texcoord;

    o.normal_dir   = normalize(UnityObjectToWorldNormal(v.normal));
    o.tangent_dir  = normalize(UnityObjectToWorldDir(v.tangent.xyz));
    o.binormal_dir = normalize(cross(o.normal_dir, o.tangent_dir) * v.tangent.w);
    o.pos_world    = mul(unity_ObjectToWorld, v.vertex).xyz;

    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);
    half4 comp_mask          = tex2D(_CompMask, i.uv);

    half  roughness  = saturate(_RoughnessAdjust + comp_mask.r);
    half  metal      = saturate(_MetalAdjust + comp_mask.g);
    half  skin_area  = 1.0 - comp_mask.b;

    half3 base_color = albedo_color.rgb * (1.0 - metal);      // 固有色
    half3 spec_color = lerp(0.0, albedo_color.rgb, metal);   // 高光色

    half3 normal_data = UnpackNormal(tex2D(_NormalMap, i.uv));

    // ---------- 方向向量 ----------
    half3 view_dir    = normalize(_WorldSpaceCameraPos.xyz - i.pos_world);
    half3 normal_dir  = normalize(i.normal_dir);
    half3 tangent_dir = normalize(i.tangent_dir);
    half3 binormal_dir = normalize(i.binormal_dir);

    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;

    // LUT 次表面散射
    half2 uv_lut     = half2(diff_term * atten + _SSSOffset, 1.0);
    half3 lut_color_gamma = tex2D(_SkinLUT, uv_lut);
    half3 lut_color  = pow(lut_color_gamma, 2.2);
    half3 sss_diffuse = lut_color * _LightColor0.rgb * base_color * half_lambert;

    #ifdef _DIFFUSECHECK_ON
        half3 direct_diffuse = lerp(common_diffuse, sss_diffuse, skin_area);
    #else
        half3 direct_diffuse = half3(0.0, 0.0, 0.0);
    #endif

    // ---------- 直接光的镜面反射 ----------
    half3 half_dir = normalize(light_dir + view_dir);
    half  NdotH    = dot(normal_dir, half_dir);

    half smoothness  = 1.0 - roughness;
    half shininess   = lerp(1, _SpecShininess, smoothness);
    half spec_term   = pow(max(0.0, NdotH), shininess * smoothness);

    #ifdef _SPECCHECK_ON
        half3 direct_specular = spec_term * spec_color * _LightColor0.xyz * atten;
    #else
        half3 direct_specular = half3(0.0, 0.0, 0.0);
    #endif

    // ---------- 间接光的漫反射(SH) ----------
    #ifdef _SHCHECK_ON
        float3 env_diffuse = custom_sh(normal_dir) * base_color * half_lambert;
        env_diffuse = lerp(env_diffuse * 0.5, env_diffuse * 0.8, skin_area);
    #else
        float3 env_diffuse = 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 * spec_color * half_lambert;
    #else
        half3 env_specular = half3(0.0, 0.0, 0.0);
    #endif

    // ---------- 最终合成 ----------
    float3 final_color = direct_specular + env_diffuse * 0.5 + direct_diffuse + env_specular;

    final_color = ACES_Tonemapping(final_color);
    final_color = pow(final_color, 1.0 / 2.2);

    return float4(final_color, 1.0);
}
ENDCG

关键公式解读

金属/粗糙度分离

base_color = albedo * (1 - metal)
spec_color = lerp(0, albedo, metal)

直接高光

shininess = lerp(1, _SpecShininess, smoothness)
spec_term = pow(max(0.0, NdotH), shininess * smoothness)

基于 Blinn-Phong 模型,通过 smoothness 控制高光锐利度。

次表面散射

uv_lut = float2(diff_term * atten + _SSSOffset, 1.0)
sss_diffuse = lut_color * _LightColor0.rgb * base_color * half_lambert

用二维 LUT 预积分漫反射穿透效果,再通过 comp_mask.b 控制皮肤区域强度。


Shader Feature 控制

宏 作用
_DIFFUSECHECK_ON 开启 Lambert + LUT-SSS 漫反射
_SPECCHECK_ON 开启 Blinn-Phong 直接高光
_SHCHECK_ON 开启球谐环境漫反射
_IBLCHECK_ON 开启 IBL 环境镜面反射

各开关效果示意

_DIFFUSECHECK_ON _SPECCHECK_ON
开启漫反射 + LUT 次表面散射 开启 Blinn-Phong 直接高光
漫反射效果 直接高光效果
_SHCHECK_ON _IBLCHECK_ON
开启球谐环境漫反射 开启 IBL 环境镜面反射
球谐环境光效果 IBL 镜面反射效果