写实皮肤渲染
写实皮肤渲染
开个头
最近在看皮肤渲染相关的渲染知识,发现这玩意还挺有意思的。于是想整理一下皮肤渲染的知识点。
皮肤渲染不是短期就可以领悟的事情,需要一段时间的知识积累,把各种效果一层层地加上去,才能呈现出整体看起来很透、很丝滑、很好看的效果。从远处看皮肤看起来很棒,仔细一瞅还有好多微小的细节,比如双层高光、毛孔、肌理,甚至汗毛等。
写实皮肤渲染的技术点汇总
写实皮肤渲染的技术难点有很多。大致罗列出以下7点
- 高光 (Specular BRDF):光源直接反射在表面上形成的高亮点,对于模拟皮肤的油脂感和光泽至关重要。
- 次表面散射 (SSS): 模拟光线在皮肤内部发生散射,关键用于捕捉皮肤透明度和自然外观。
- 法线混合(bent normals):根据Diffusion Profiles方法与光线波长的关系,进行细分法线,对次表面散射和界面反射进行柔和处理。
- 细节贴图:通过法线贴图、置换贴图等技术,添加微小细节,如毛孔、血管,提高外观的真实感。
- Cavity Map:模拟皮肤上深浅变化,增加细节和层次感,使皮肤看起来更为自然。
- 环境光遮挡 (AO):模拟由于周围环境光的遮挡而引起的阴影效果,增加表面微小细节,提供深度感。
1. f r \huge f_r fr镜面反射(Specualr BRDF)
Specular镜面反射
- f r \huge f_r fr是镜面反射(Specular Reflection), r \huge _r r表示的是高光反射函数Reflectance Function首字母,用 f r \huge f_r fr中的 r \huge _r r这个符号是一种惯例,用来表示反射函数(Reflectance Function)中的一部分,包括镜面反射。
如图:蓝色的是 f r \huge f_r fr,黄色的是 L \huge L L:
- f d \huge f_d fd是漫反射(Diffuse),右下角的 d \huge _d d表示是Diffuse的首字母缩写。
(补坑:Cook-Torrance BRDF(Bidirectional Reflectance Distribution Function)2024.04.07)
补坑链接:
【PBR基于微平面理论的Microfacet Cook-Torrance BRDF知识点分享_UE虚幻和Unity的渲染规则解析_第一篇】
下图是部分内容截图:
UE4的SpecularGGX镜面反射
float3 SpecularGGX( float Roughness, float3 SpecularColor, BxDFContext Context, float NoL, FAreaLight AreaLight )
{
float a2 = Pow4( Roughness );//相当于Roughness * Roughness * Roughness * Roughness
float Energy = EnergyNormalization( a2, Context.VoH, AreaLight );//能量守恒原则
// Generalized microfacet specular
float D = D_GGX( a2, Context.NoH ) * Energy;
float Vis = Vis_SmithJointApprox( a2, Context.NoV, NoL );
float3 F = F_Schlick( SpecularColor, Context.VoH );//迪士尼BRDF的F项计算
return (D * Vis) * F;
}
基于GGX(Trowbridge-Reitz)微表面模型的镜面反射颜色计算
Pow4
函数: 该函数对输入值进行四次方操作。EnergyNormalization
函数: 能量归一化函数,用于计算微表面模型的归一化项,以确保反射的能量在不同粗糙度下保持一致。D_GGX
函数: GGX(Trowbridge-Reitz)分布函数,用于计算法线分布函数。在这里,它使用了给定的粗糙度Roughness
和法线与半角向量夹角Context.NoH
。Vis_SmithJointApprox
函数: 这是一个计算Smith遮挡函数的函数,用于估计两个粗糙度下的能见度。这里使用了给定的粗糙度Roughness
,视角与法线夹角Context.NoV
和入射光线与法线夹角NoL
。F_Schlick
函数: Schlick近似的菲涅尔反射项,用于计算菲涅尔反射颜色。- 将计算得出的分布函数
D
、遮挡函数Vis
和菲涅尔反射颜色F
组合在一起。
在虚幻4中迪士尼BRDF的 F项
计算公式
如下:
// 迪士尼BRDF的F项计算
double F_Schlick(double _F0, double _F90, double _cosTheta)
{
double x = 1.0 - _cosTheta;
double x2 = x * x;
double x5 = x * x2 * x2;
return (_F90 - _F0) * x5 + _F0;// sub mul mul mul sub mad
}
(补坑:PBS(Physically Based Specular)2024.04.07)
PBR渲染模型的**
等待补充
**链接:
补坑链接:
【PBR基于微平面理论的Microfacet Cook-Torrance BRDF知识点分享_UE虚幻和Unity的渲染规则解析_第一篇】
Specular Lobe和Dual Specular Lobe效果图
Dual Specular Lobe也即DualSpecularGGX双层镜面反射。
下面是一层镜面反射和双层镜面反射的对比效果。如图:
在UE4的DualSpecularGGX双层镜面反射
float3 DualSpecularGGX(float AverageRoughness, float Lobe0Roughness, float Lobe1Roughness, float LobeMix, float3 SpecularColor, BxDFContext Context, float NoL, FAreaLight AreaLight)
{
float AverageAlpha2 = Pow4(AverageRoughness);
float Lobe0Alpha2 = Pow4(Lobe0Roughness);
float Lobe1Alpha2 = Pow4(Lobe1Roughness);
float Lobe0Energy = EnergyNormalization(Lobe0Alpha2, Context.VoH, AreaLight);
float Lobe1Energy = EnergyNormalization(Lobe1Alpha2, Context.VoH, AreaLight);
// Generalized microfacet specular
float D = lerp(D_GGX(Lobe0Alpha2, Context.NoH) * Lobe0Energy, D_GGX(Lobe1Alpha2, Context.NoH) * Lobe1Energy, LobeMix);
float Vis = Vis_SmithJointApprox(AverageAlpha2, Context.NoV, NoL); // Average visibility well approximates using two separate ones (one per lobe).
float3 F = F_Schlick(SpecularColor, Context.VoH);
return (D * Vis) * F;
}
这段代码是一个用于计算基于Dual Specular GGX(广义的GGX双高光)的镜面反射颜色的函数。
以下是代码的主要部分的简要解释:
Pow4
函数: 它对输入值进行四次方操作。EnergyNormalization
函数: 该函数用于能量归一化,计算微表面模型的归一化项,以确保镜面反射的能量在不同粗糙度下保持一致。D_GGX
函数: GGX(Trowbridge-Reitz)分布函数,用于计算法线分布函数。在这里,它分别应用于两个不同的粗糙度值(Lobe0Roughness
和Lobe1Roughness
)。lerp
函数: 线性插值函数,用于在两个粗糙度之间进行插值,根据LobeMix
的权重。Vis_SmithJointApprox
函数: 这是一个计算Smith遮挡函数的函数,用于估计两个粗糙度下的能见度。这里使用一个平均粗糙度(AverageRoughness
)来进行估算。AverageRoughness 计算与(Lobe0Roughness + Lobe1Roughness)*0.5结合起来。F_Schlick
函数: Schlick近似的菲涅尔反射项,用于计算菲涅尔反射颜色。
在上面的DualSpecularGGX结构中,可以将AverageRoughness 计算与(Lobe0Roughness + Lobe1Roughness)*0.5结合起来。
AverageRoughness
AverageRoughness = (Lobe0Roughness + Lobe1Roughness)*0.5;
双层镜面反射保持能量守恒定律,两个镜面反射的线性混合。
Lobe 1——主要控制,通常为85%。
Lobe 2——次要控制,比Lobe1要光滑一点,第二道15%。
2.次表面散射(SSS)
实时次表面散射技术(Real-Time Subsurface Scattering,简称RTSSS),是模拟光线穿透物体表面后在物体内部发生散射的现象。这个现象常常在透明或半透明的物体中观察到,比如人类的皮肤、蜡烛透光的部分。
次表面散射是一种光线穿过物体表面后在物体内部发生多次反射和散射的过程。在皮肤渲染中,这是非常重要的,因为人体皮肤是半透明的,光线不仅会在表面反射,还会在皮肤内部产生一些变化。这种效果对于模拟皮肤的外观至关重要,尤其是在受到透明性影响的部分,例如耳朵、鼻子、手指等。
在实时次表面散射中,目标是在计算机图形的实时渲染环境下模拟这种效果。这对于视频游戏、虚拟现实和其他实时图形应用非常重要,因为传统的计算密集型渲染技术在这些场景中无法实现流畅的渲染。实时次表面散射需要高效的算法和技术,以在实时性要求下模拟逼真的皮肤效果,提升场景的真实感。
参考文章链接
An Introduction To Real-Time Subsurface Scattering实时次表面散射
Next-Generation-Character-Rendering(GDC2013).PPT文件下载地址
Diffusion Profiles方法与光线波长关系
Diffusion Approximation 基于扩散的近似方法。使用Diffusion Profiles扩散配置文件是在实现次表面散射(SSS)方面的一种重要方法。Diffusion Profiles主要用于描述光线在物体内部传播时的散射行为,特别是光线与散射距离之间的关系。
这通过定义三条曲线,即红R、绿G、蓝B三种光波长,以描述入射点距离的变化而引起的衰减关系。
如上图:
这些曲线形成了Diffusion Profiles,提供了对光在物体内部传播时如何被吸收和散射的详细了解。每种波长的光都会在不同的距离内产生不同的衰减,这反映了物体材质对不同光波长的吸收和散射特性。通过使用Diffusion Profiles可以更准确地模拟光在皮肤、蜡像等材质中的传播过程,从而实现更加真实和细致的次表面散射效果。
Diffusion Profiles扩散配置文件
通过三个参数来控制Diffusion Profile,从而实现多种皮肤效果。
Width(宽度)
: Width参数表示滤波器的总宽度。它影响光线在物体内部进行次表面散射的距离。具体而言,数值越大,光线可以从更远的距离散射出去,因此,它决定了渗色的范围。较大的Width值意味着光线可以在物体内更远的距离上发生散射。RGB Falloff(颜色衰减)
: RGB Falloff参数是由RGB三个分量组成的颜色参数。每个分量分别控制着光线在不同波长上的衰减。这一参数可以被简单理解为影响散射的颜色。通过调整RGB Falloff,你可以定制散射的颜色,使其更符合所需的外观。Strength(强度)
: Strength参数定义了有多少光线进入了物体内部参与次表面散射。较大的Strength值表示更多的光线会被物体吸收并发生次表面散射。这一参数的调整直接影响到最终的渲染效果,数值越大通常会导致更柔和(模糊)的效果,增强次表面散射的外观。
Diffusion Profiles是一个可以自定义的配置文件,在Unity中创建HDRP Diffusion Profiles文件。如图:
在Unity中实现具体的次表面散射(SSS)效果
采样曲率图:
float NoL = dot(normalize(normalWS),lightWS) * 0.5 + 0.5;
float2 UV = float2(NoL,Curvature);
float3 SSSLUTmap = SAMPLE_TEXTURE2D(SSSLUT,sampler_SSSLUT,UV).rgb;
float3 col = SSSLUTmap * LightColor;
简易低消耗的次表面散射效果
左侧为带有的LightingTranslucent人物模型,右侧为正常的Lit.shader。可以看出,左侧的人物耳朵有透光效果。
低消耗的次表面散射效果
half3 LightingTranslucent(float3 lightDir,float3 viewDir,half3 normalWS,half3 color,half thickness)
{
half3 L = lightDir;
half3 V = viewDir;
half3 N = normalWS;
half3 H = L + N * _NormalDistortion;
half _LdotV = dot(-H,V);
half3 I = pow(saturate(_LdotV),_Attenuation) * _Strength;
I *= thickness;
I *= color;
return I;
}
L
、V
、N
和H
的定义: 定义了光照方向(L
)、视线方向(V
)、法线(N
)和半角向量(H
)。_NormalDistortion
是一个可能在着色器中定义的参数,用于调整法线的。_LdotV
的计算: 计算光照方向与视线方向的半角余弦值,这是计算镜面高光的一部分。I
的计算: 使用_LdotV
计算光照的衰减,应用_Attenuation
参数和_Strength
参数。这里saturate
函数用于确保值在 [0, 1] 范围内。I *= thickness
: 将计算得到的光照值乘以材质的厚度(thickness
),考虑了材质的透明度,thickness
可以是烘焙出的贴图,信息,单通道既可。I *= color
: 将计算得到的光照值乘以材质的颜色,以考虑材质对光的吸收和反射。- 最后,函数返回计算得到的光照值
I
。
3.Bent normals
使用三种不同模糊程度的法线图分别计算R、G、B三种波长的漫反射光照,进一步模拟Diffusion Profiles
Bent normals采样原理:
- Specular Normals采样的是法线贴图Mip Level 0的原始法线。
- R通道normal采样的是法线贴图Mip Level 4的模糊后的法线。
采样代码如下:
float3 R通道 = UnpackNormalScale( SAMPLE_TEXTURE2D_LOD( _Normal, sampler_Normal, uv, 4.0 ), _NormalStrength );
- BG通道是通过lerp对原始法线和Mip Level 4的法线进行差值操作。B值更靠近原始法线,G值靠近Mip Level 4的法线。
4.细节贴图
Detail Normal:对于脸部法线,通常主要包含宏观褶皱和整体表面的几何信息,
而微观的毛孔、细小的纹理等细节则通过Detail Normal来表现。
下图就是整个面部的法线贴图(BaseNormal Map)和细节贴图(Detail Normal)
Detail Normal是辅助基础法线贴图的细节法线贴图,通常使用tiling(平铺)的方式达到高精度细节纹理,而不用采样一张非常大尺寸的。这种技术使得可以高效地表现细小的纹理,如毛孔、细微的褶皱和皮肤上的微小瑕疵。
5.Cavity Map
看似是用来表现皮肤微观细节的AO信息,其实主要是specular occlusion,作用在于镜面反射遮挡项,白话就是打撒皮肤微观高光。
如下图:
CavityMap前后对比:
可以看到使用CavityMap,进一步细化了高光反射,使皮肤有更多微观高光细节,正如前面说的其实本质就是打撒皮肤微观高光,使肌理更真实。
Cavity贴图不能直接当成AO那样粗暴计算,而是应该当成
SpecularMap
,直接控制反射率强弱。
6.环境光遮挡AO
BakeAO——Spec Occlusion/Color-Bleed / MultiBounce
SSAO/HBAO——Color-Bleed
在Unity URP SSAO的获取
主要代码如下:
#if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT)
float2 normalizedScreenSpaceUV = GetNormalizedScreenSpaceUV(input.positionCS);
AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(normalizedScreenSpaceUV);
finalColor.rgb *= aoFactor.indirectAmbientOcclusion;
#endif
-
GetNormalizedScreenSpaceUV
函数: 该函数获取屏幕空间UV坐标,通常是将输入的世界空间坐标转换为屏幕空间坐标,并将其标准化。 -
GetScreenSpaceAmbientOcclusion
函数: 该函数使用标准化的屏幕空间UV坐标获取屏幕空间的环境光遮挡因子。这通常是通过对屏幕空间的深度信息执行采样来估算场景中物体表面的环境光遮挡。 -
AmbientOcclusionFactor
结构体: 这似乎是一个结构体,包含有关环境光遮挡因子的信息。 -
aoFactor.indirectAmbientOcclusion
: 它是从环境光遮挡因子结构体中提取的间接环境光遮挡的分量。 -
将
finalColor.rgb
与aoFactor.directAmbientOcclusion
相乘,将直接环境光遮挡因子应用于finalColor
,从而调整了最终的颜色。
下面这是AmbientOcclusion.hlsl文件代码,如下:
#ifndef AMBIENT_OCCLUSION_INCLUDED
#define AMBIENT_OCCLUSION_INCLUDED
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/SurfaceData.hlsl"
// Ambient occlusion
TEXTURE2D_X(_ScreenSpaceOcclusionTexture);
SAMPLER(sampler_ScreenSpaceOcclusionTexture);
struct AmbientOcclusionFactor
{
half indirectAmbientOcclusion;
half directAmbientOcclusion;
};
half SampleAmbientOcclusion(float2 normalizedScreenSpaceUV)
{
float2 uv = UnityStereoTransformScreenSpaceTex(normalizedScreenSpaceUV);
return half(SAMPLE_TEXTURE2D_X(_ScreenSpaceOcclusionTexture, sampler_ScreenSpaceOcclusionTexture, uv).x);
}
AmbientOcclusionFactor GetScreenSpaceAmbientOcclusion(float2 normalizedScreenSpaceUV)
{
AmbientOcclusionFactor aoFactor;
#if defined(_SCREEN_SPACE_OCCLUSION) && !defined(_SURFACE_TYPE_TRANSPARENT)
float ssao = SampleAmbientOcclusion(normalizedScreenSpaceUV);
aoFactor.indirectAmbientOcclusion = ssao;
aoFactor.directAmbientOcclusion = lerp(half(1.0), ssao, _AmbientOcclusionParam.w);
#else
aoFactor.directAmbientOcclusion = 1;
aoFactor.indirectAmbientOcclusion = 1;
#endif
#if defined(DEBUG_DISPLAY)
switch(_DebugLightingMode)
{
case DEBUGLIGHTINGMODE_LIGHTING_WITHOUT_NORMAL_MAPS:
aoFactor.directAmbientOcclusion = 0.5;
aoFactor.indirectAmbientOcclusion = 0.5;
break;
case DEBUGLIGHTINGMODE_LIGHTING_WITH_NORMAL_MAPS:
aoFactor.directAmbientOcclusion *= 0.5;
aoFactor.indirectAmbientOcclusion *= 0.5;
break;
}
#endif
return aoFactor;
}
AmbientOcclusionFactor CreateAmbientOcclusionFactor(float2 normalizedScreenSpaceUV, half occlusion)
{
AmbientOcclusionFactor aoFactor = GetScreenSpaceAmbientOcclusion(normalizedScreenSpaceUV);
aoFactor.indirectAmbientOcclusion = min(aoFactor.indirectAmbientOcclusion, occlusion);
return aoFactor;
}
AmbientOcclusionFactor CreateAmbientOcclusionFactor(InputData inputData, SurfaceData surfaceData)
{
return CreateAmbientOcclusionFactor(inputData.normalizedScreenSpaceUV, surfaceData.occlusion);
}
#endif
屏幕空间AO(Screen Space Ambient Occlusion,SSAO)
-
TEXTURE2D_X(_ScreenSpaceOcclusionTexture)
和SAMPLER(sampler_ScreenSpaceOcclusionTexture)
: 定义了用于存储屏幕空间环境光遮挡信息的纹理和采样器。 -
struct AmbientOcclusionFactor
: 定义了一个结构体,包含两个half
类型的成员,分别表示间接环境光遮挡和直接环境光遮挡的因子。 -
half SampleAmbientOcclusion(float2 normalizedScreenSpaceUV)
: 该函数通过对纹理进行采样获取屏幕空间环境光遮挡的值。 -
AmbientOcclusionFactor GetScreenSpaceAmbientOcclusion(float2 normalizedScreenSpaceUV)
: 该函数获取屏幕空间环境光遮挡因子,调用了SampleAmbientOcclusion
函数。根据是否启用了屏幕空间环境光遮挡和是否是透明表面,会进行相应的处理。 -
AmbientOcclusionFactor CreateAmbientOcclusionFactor(float2 normalizedScreenSpaceUV, half occlusion)
: 该函数通过调用GetScreenSpaceAmbientOcclusion
函数创建一个AmbientOcclusionFactor
结构体,同时限制了间接环境光遮挡的值。 -
AmbientOcclusionFactor CreateAmbientOcclusionFactor(InputData inputData, SurfaceData surfaceData)
: 这是另一个用于创建AmbientOcclusionFactor
结构体的函数,接收输入数据和表面数据。