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

主要特性
- 金属/粗糙度工作流
- 切线空间法线贴图
- LUT 驱动的次表面散射
- 球谐环境光
- 基于粗糙度的 IBL 环境镜面反射
- ACES 色调映射 + Gamma 校正输出
- 通过 shader feature 控制漫反射、直接高光、SH 与 IBL
球谐环境光
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)
- 非金属部分:漫反射颜色使用固有色
base_color。 - 金属部分:镜面反射颜色使用反照率
albedo。
直接高光
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 环境镜面反射 |
![]() |
![]() |



