真实感皮肤渲染
本文主要结合了预积分次表面散射(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 实现细节
-
主波瓣(Primary Lobe):使用GGX分布,模拟油脂层的柔和高光。

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

-
高光遮罩(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 实现细节
-
基础透射:根据厚度和透射强度计算基础透射值。
-
方向性:只计算背光区域(NdotL < 0)。
-
视角因子:从背面看透射更强。
-
光线扭曲:模拟光线在皮肤内的散射,使光线方向发生偏移。
-
曲率影响:曲率大的地方(薄)透射更强。
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);
参考文章
只是做一个小小的记录,还有许多没有考虑到,有不足的欢迎感谢大佬的文章
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)