本文主要结合了预积分次表面散射(SSS)、双叶高光、透射、SSS阴影技术在unity的URP管线中实现的,有很多地方做了艺术化处理,并不完全写实。版本2022.3.19

  • 预积分次表面散射(SSS):模拟光在皮肤下的散射,使用LUT和多重散射法线。

  • 双叶高光模型:使用两个高光波瓣(GGX和Beckmann)分别模拟皮肤油脂层和角质层。

  • 透射效果:模拟背光时皮肤透光的效果,基于厚度和视角。

  • SSS阴影:将次表面散射与阴影结合,实现自然的阴影过渡。

  • 曲率增强:使用曲率图增强细节和散射效果。

  • 包裹光照:模拟皮肤柔和的漫反射。

  • 边缘光(Rim Light):增强轮廓效果。

用到的贴图如下:

纹理名称 说明 通道用途
_BaseMap 反照率(漫反射颜色) RGB: 反照率, A: 透明度(可选)
_NormalMap 切线空间法线贴图 RGB: 法线向量
_AOMap 环境光遮蔽贴图 R: 环境光遮蔽
_SSSLut 预积分SSS查找表 RGB: 分别对应R、G、B波长的散射
_ThicknessMap 厚度贴图 R: 厚度(通常为0-1,1表示厚)
_CurvatureMap 曲率贴图 R: 曲率(0-1,1表示高曲率)

1. 预积分皮肤着色(Pre-Integrated Skin Shading)

预积分皮肤着色是一种实时次表面散射(SSS)技术,光从 正面 照过来,在皮肤浅层散射后从 入射点附近 逸出。效果是让亮面变柔和,暗面泛红。它通过使用一张预计算的查找纹理(LUT)来模拟光在皮肤表面的散射。LUT的U坐标是半兰伯特(N·L)的值,V坐标是曲率和厚度的混合。使用三个不同的法线(对应R、G、B通道,模糊程度不同)分别采样LUT,模拟不同波长的散射距离。

1.1 原理

皮肤是半透明材质,光线进入皮肤后会在内部散射,然后从另一个点射出。这使得皮肤在边缘和阴影区域呈现出柔和的外观和颜色偏移(通常偏向红色)。

预积分方法假设表面是弯曲的,并且散射是各向同性的。它使用曲率和厚度来索引LUT,从而快速得到散射后的颜色。

1.2 实现

1.需要用到法线贴图、厚度贴图和曲率贴图。厚度贴图表示皮肤各处的厚度(通常耳朵、鼻子较薄,脸颊较厚)。曲率贴图表示表面的弯曲程度(PT里面可以烘培)。

2.法线混合红、绿、蓝光的散射距离不同(红光穿透最深,散射半径最大),所以三个通道的“有效法线”应该有不同程度的平滑,模拟皮下不同深度的散射。用来分别计算红、绿、蓝通道的光照。

3.计算半兰伯特(Half Lambert):半兰伯特模拟漫反射,将兰伯特模型的点积结果从[-1,1]映射到[0,1]来增强暗部细节。

4.LUT采样:使用半兰伯特值和曲率-厚度的混合值作为UV坐标来采样LUT。LUT的V坐标由曲率和厚度共同决定,U坐标是半兰伯特值。

float3 CalculatePreIntegratedSSS(float3 normalWS, float3 normalWSSmooth, float3 lightDir,       float curvature,  // 曲率float thickness,   // 厚度
    float3 scatterColor,     
    float sssStrength,      
    float thicknessMin,       
    float thicknessMax,       
    float curveMax,           
    float3 blurFactors      
)
{
    // 重映射厚度和曲率到0-1范围
    float remappedThickness = saturate((thickness - thicknessMin) / max(thicknessMax - thicknessMin, 0.001));
    float remappedCurvature = saturate((curvature - curveMin) / max(curveMax - curveMin, 0.001));

    // 创建三个不同模糊程度的法线,分别用于RGB通道
    float3 normalR = normalize(lerp(normalWS, normalWSSmooth, blurFactors.x));
    float3 normalG = normalize(lerp(normalWS, normalWSSmooth, blurFactors.y));
    float3 normalB = normalize(lerp(normalWS, normalWSSmooth, blurFactors.z));

    float NdotL_R = dot(normalR, lightDir);
    float NdotL_G = dot(normalG, lightDir);
    float NdotL_B = dot(normalB, lightDir);

    float halfLambert_R = saturate(NdotL_R * 0.5 + 0.5);
    float halfLambert_G = saturate(NdotL_G * 0.5 + 0.5);
    float halfLambert_B = saturate(NdotL_B * 0.5 + 0.5);

    // 计算LUT的V坐标:曲率和厚度的混合
    float lutV = saturate(remappedCurvature * 0.7 + remappedThickness * 0.3);
    lutV = pow(lutV, 1.5);

    // 分别采样RGB通道
    float2 uv_R = float2(halfLambert_R, lutV);
    float2 uv_G = float2(halfLambert_G, lutV);
    float2 uv_B = float2(halfLambert_B, lutV);

    float sss_R = SAMPLE_TEXTURE2D_LOD(_SSSLUT, sampler_SSSLUT, uv_R, 0).r;
    float sss_G = SAMPLE_TEXTURE2D_LOD(_SSSLUT, sampler_SSSLUT, uv_G, 0).g;
    float sss_B = SAMPLE_TEXTURE2D_LOD(_SSSLUT, sampler_SSSLUT, uv_B, 0).b;

    
    float3 sss = float3(sss_R, sss_G, sss_B);

 
    sss *= scatterColor * sssStrength;

    // 基于厚度的衰减
    float thicknessAtten = 1.0 - saturate(remappedThickness * 0.8);
    sss *= thicknessAtten;

    // 基于曲率的增强
    float curvatureBoost = 1.0 + remappedCurvature * 0.5;
    sss *= curvatureBoost;

    return sss;
}

2. 双叶高光(Dual Lobe Specular)

皮肤的高光反射通常由两个波瓣组成:一个宽而柔和的波瓣(来自皮肤表面的油脂层)和一个窄而锐利的波瓣(来自皮肤表面的角质层)。双叶高光模型就是用来模拟这种效果的。(偷一下图,如有侵权,联系删除)

2.1 原理

使用两个不同的微表面分布函数(通常是GGX和Beckmann)来分别模拟这两个波瓣。然后,将这两个波瓣的高光结果按照一定权重混合。

2.2 实现细节

  1. 主波瓣(Primary Lobe):使用GGX分布,模拟油脂层的柔和高光。

  2. 次波瓣(Secondary Lobe):使用Beckmann分布,模拟角质层的锐利高光。

  3. 高光遮罩(Specular Mask):使用一张遮罩贴图来控制次波瓣的强度,通常在高光区域(如鼻尖、额头)次波瓣更强。

// GGX分布函数
float GGXDistribution(float NdotH, float roughness)
{
    float a2 = roughness * roughness;
    float d = (NdotH * a2 - NdotH) * NdotH + 1.0;
    return a2 / (PI * d * d + 1e-7);
}

// Beckmann分布函数
float BeckmannDistribution(float NdotH, float roughness)
{
    float a = roughness;
    float a2 = a * a;
    float NdotH2 = NdotH * NdotH;

    if (NdotH2 < 1e-5) return 0.0;

    float cos2 = NdotH2;
    float tan2 = (1.0 - cos2) / cos2;

    float D = exp(-tan2 / a2) / (PI * a2 * cos2 * cos2);
    return D;
}

// 几何遮蔽项(Smith GGX)
float SmithGGXGeometry(float NdotV, float roughness)
{
    float a = roughness * roughness;
    float k = a / 2.0; // 对于直接光照的Smith GGX
    return NdotV / (NdotV * (1.0 - k) + k);
}

// 菲涅尔项(Schlick近似)
float3 SchlickFresnel(float3 F0, float LdotH)
{
    float power = pow(1.0 - saturate(LdotH), _FresnelPower);
    return F0 + (1.0 - F0) * power;
}


float3 DualLobeSpecularBRDF(
    float3 specularColor,
    float roughness,
    float secondaryRoughness,
    float lobeWeight,
    float specularMask,
    float NdotL,
    float NdotV,
    float NdotH,
    float LdotH
)
{
    // 计算几何项
    float G = SmithGGXGeometry(NdotL, roughness) * SmithGGXGeometry(NdotV, roughness);
    
    // 计算菲涅尔项
    float3 F = SchlickFresnel(specularColor, LdotH);
    
    // 主波瓣 (GGX)
    float D_primary = GGXDistribution(NdotH, roughness);
    float3 specularTermGGX = D_primary * G * F / (4.0 * NdotL * NdotV + 1e-5);
    
    // 次波瓣 (Beckmann)
    float D_secondary = BeckmannDistribution(NdotH, secondaryRoughness);
    float3 specularTermBeckmann = D_secondary * G * F * lobeWeight / (4.0 * NdotL * NdotV + 1e-5);
    
    specularTermBeckmann *= pow(specularMask, _SpecularMaskPower);
    float3 specularTerm = (specularTermGGX + specularTermBeckmann);
    

    specularTerm = min(specularTerm, 10.0);
    
    return specularTerm;
}


float3 SkinDualLobeSpecular(
    float3 specularColor,
    float roughness,
    float secondaryRoughness,
    float lobeWeight,
    float specularMask,
    float NdotL,
    float NdotV,
    float NdotH,
    float LdotH,
    float curvature,
    float thickness,
    float3 viewDirWS,
    float3 normalWS
)
{
    // 基础双叶高光
    float3 specular = DualLobeSpecularBRDF(
        specularColor,
        roughness,
        secondaryRoughness,
        lobeWeight,
        specularMask,
        NdotL,
        NdotV,
        NdotH,
        LdotH
    );
    

    float curvatureAttenuation = 1.0 - saturate(curvature * 0.5);
    specular *= curvatureAttenuation;
    

    float thicknessInfluence = 1.0 - saturate(thickness * 0.5);
    specular *= lerp(1.0, 0.8, thicknessInfluence);
    

    float grazingFactor = pow(1.0 - NdotV, 3.0);
    specular *= 1.0 + grazingFactor * 0.5;
    

    specular *= saturate(NdotL + 0.1);
    
    return specular;
}

3. 透射(Transmission)

透射模拟了光线穿过半透明材质(如皮肤)的效果,在背光区域,皮肤会显得更红更亮。光从 背面 照过来,穿透整个薄组织,从 观察面 射出。效果是背光时薄边缘发光(耳朵、鼻翼发红)。

3.1 原理

使用厚度贴图和光线方向来计算透射强度。通常使用Beer-Lambert定律来模拟光在介质中的衰减。

3.2 实现细节

  1. 基础透射:根据厚度和透射强度计算基础透射值。

  2. 方向性:只计算背光区域(NdotL < 0)。

  3. 视角因子:从背面看透射更强。

  4. 光线扭曲:模拟光线在皮肤内的散射,使光线方向发生偏移。

  5. 曲率影响:曲率大的地方(薄)透射更强。

float3 CalculateSkinTransmission(
    float thickness,        
    float3 normalWS,        
    float3 lightDir,         
    float3 viewDir,         
    float3 scatterColor,     
    float transmissionStrength, // 透射强度
    float transmissionPower,  
    float distortion,         // 光线扭曲程度
    float curvature           // 曲率(影响透射分布)
)
{

    float baseTransmission = exp(-thickness * transmissionPower);

    // 只在背光时计算
    float NdotL = dot(normalWS, lightDir);
    if (NdotL >= 0) return float3(0, 0, 0); // 正面光照不计算透射

    //从背面看透射更强
    float VdotL = dot(viewDir, -lightDir);

    // 模拟皮下散射
    float3 distortedLightDir = lightDir + normalWS * distortion;
    float distortionFactor = saturate(dot(-distortedLightDir, viewDir))

    float curvatureFactor = 1.0 + curvature * 2.0; // 曲率大的地方(薄)增强透射

    float transmission = baseTransmission * 
                        abs(NdotL) *     
                        viewFactor *     
                        distortionFactor * 
                        curvatureFactor *
                        

 
    transmission = pow(transmission, 1.5) * transmissionStrength;

    // 皮肤透射偏红色
    float3 transmissionColor = scatterColor;

    // 根据厚度调整 薄处更红
    float redBoost = 1.0 + (1.0 - thickness) * 2.0;
    transmissionColor.r *= redBoost;

    return transmissionColor * transmission;
}

4.其他

 包裹光照(Wrap Lighting)

包裹光照是一种模拟区域光照的技术,通过将光照方向“包裹”到模型表面,使得暗部更加柔和。

float wrappedDiffuse = saturate(((NdotL*0.5+0.5) + _WrapIntensity) / (1.0 + _WrapIntensity));
wrappedDiffuse = pow(wrappedDiffuse, _WrapPower);

参考文章

 Unity实现预积分皮肤次表面散射 - 知乎

[总结]皮肤渲染-预积分方案 - 知乎

 Unity移动端写实角色渲染-皮肤(URP) - 知乎

Unity写实人物渲染(二)——皮肤 - 知乎

【真实感人物渲染】(一)皮肤篇 - 知乎

只是做一个小小的记录,还有许多没有考虑到,有不足的欢迎感谢大佬的文章

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐