剖析Unreal Engine超真实人类的渲染技术Part 2 - 眼球渲染

三、眼球渲染

都说眼睛是人类心灵的窗户,若是眼睛渲染得逼真,将给虚拟角色点睛之笔,给予其栩栩如生的灵魂。

Mike那深邃的眼眸,唏嘘的胡渣子,神乎其神的眼神。。。应该征服了不少迷妹

再来一张超近距离的特写:

超近距离的眼睛特写,细节刻画得无与伦比,足以以假乱真。

然而,要渲染出如此逼真有神的眼睛,可不是那么简单,需要经过多道工序,运用许多渲染技法,刻画很多细节。

3.1 眼球的构造及理论

3.1.1 眼球的构造

生物学的眼球解剖图非常复杂,涉及的部位数十种。(下图)

人类眼球的生物学剖面图,涉及部位多达数十种。

在图形渲染领域,当然不可能关注这么多细节,可以将眼球构造做简化,只关注其中的几个部位:

上图所示的序号代表的部位:

  • 1 - 巩膜(sclera):也称为“眼白”,通常非常湿润,包含少量的触感纹理、血丝等细节。
  • 2 - 角膜缘(limbus):角膜缘存在于虹膜和巩膜之间的深色环形。有些眼睛中的角膜缘更为明显,从侧面看时往往会消退。
  • 3 - 虹膜(iris):虹膜是围绕在眼睛中心周围的一圈色环。如果某个人有“绿”眼睛,就是因为虹膜主要是绿色的。在真实的眼睛中,虹膜是类似肌肉的纤维结构,有扩张和收缩功能,以让更多光线进入瞳孔或者不让光线进入瞳孔。还需要注意的是,在真实世界中,虹膜实际上更像是圆盘或锥形,不会向眼部其余部分突出。
  • 4 - 瞳孔(pupil):瞳孔是眼睛中心的黑点。这是一个孔,光线穿过这个孔后才会被视网膜的视杆和视锥捕捉到。
  • 5 - 角膜(cornea):角膜是位于虹膜表面上的一层透明的、充满液体的圆顶结构。

3.1.2 眼球的渲染理论

由于眼球充满了液体,因此会折射照射进来的任何光线。在真实世界中从多个角度观察眼球时就会看到这种效果。虹膜和瞳孔会因为折射而变形,因为它们是透过角膜观看的。

游戏和电影中用来解决这个问题的传统方法是创建两层独立的眼睛表面,一层提供巩膜、虹膜和瞳孔,另一层位于顶部,提供角膜和眼睛的总体湿润度。这样底层表面透过湿润层观看时就会产生折射。

《A Boy and His Kite》中的男孩眼睛中采用的就是两层表面的渲染模型

根据上面的分析,以及简化后的眼球解剖结构,就可以得出结论,要渲染好眼睛,需要着重实现的效果包括:

  • 角膜的半透和光泽反射效果。
  • 瞳孔的次表面散射。
  • 瞳孔的缩放。最好根据整个场景的光照强度动态调整缩放大小。
  • 虹膜的颜色变化。
  • 其它眼球细节。

下节将详细探讨。

3.2 眼球的渲染技术

本节主要参考来源:

3.2.1 角膜的半透和光泽反射

角膜的半透射和反射效果最能体现眼球渲染的效果。

简单的做法就是直接把角膜看做一个半透明光泽球体的反射,正常的做法是用PBR流程计算其镜面反射和IBL反射,然后给眼球一张虹膜和眼白的贴图,这张贴图作为角膜下面的折射效果,最后给角膜设定一个混合系数,把光泽球体反射效果和虹膜及眼白贴图上的颜色进行混合。

角膜的镜面反射和环境反射丰富了眼球的细节,增加了真实可信度

3.2.2 瞳孔的次表面散射

瞳孔本身实际上也是一个高低不平有纵深感的结构,它与角膜存在一定距离。这使得瞳孔会发生折射,并且,当光线到达瞳孔表面的时候,还会进一步在瞳孔结构内部发生次表面散射。

把眼球看成了一个双层结构,外面一层是角膜,里面一层是瞳孔的表面,而角膜和瞳孔之间我们可以认为是充斥了某种透明液体。

光线在进入瞳孔组织的内部前,首先会在角膜的表面发生一次折射,然后进入瞳孔组织的内部,产生散射,最后从瞳孔表面的另一个点散射出来。这里就涉及到了两个问题:

(1)一束射到角膜表面的光线在经过折射后,如何计算最终入射到瞳孔表面的位置;

(2)光线进入角膜内部后,如何计算其散射效果。

为了解决以上两个问题,可使用次表面纹理映射(Subsurface texture mapping),这个方法旨在解决多层厚度不均匀的材质的次表面散射效果的计算。

如上图,每一层材质都有一个单独的深度图,保存在一个通道里,然后每一层单独的材质被认为是均匀的,拥有相同的散射、吸收系数以及相应的相位函数(散射相关的参数)。然后,以视线和第一层材质的交点为起点,沿着视线方向对多层材质进行ray-marching,每行进一步就根据位置和深度图计算当前点位于材质的哪一层,对应什么散射参数,再根据上一步的位置以及光照方向计算散射和吸收,直到ray-marching结束。具体到眼球的散射计算,实际上只有一层散射材质,即瞳孔材质。因此我们只需要提供瞳孔表面的深度图,并设定好瞳孔材质的相关散射参数,再结合次表面纹理映射的方法计算即可。

这部分主要涉及的渲染技术:

  • 视差贴图(parallax mapping,也叫relief mapping)。可以通过ray marching的方法结合一张深度图在相对平坦的几何表面上实现视觉正确的高低起伏效果,法线效果虽然也能在平面上产生凹凸起伏,但在比较斜的视角下平面还是平面,视差贴图则不会这样。

    左:normal mapping效果;右:parallax mapping效果。可见在倾斜视角下,后者效果要好很多。

  • 基于物理的折射(Physically based Refraction)。与视差贴图的欺骗式计算不同,基于物理的折射是根据真实的折射模型进行模拟,效果更真实。

    float cosAlpha = dot(frontNormalW, -refractedW);
    float dist = height / cosAlpha;
    float3 offsetW = dist * refractedW;
    float2 offsetL = mul(offsetW, (float3x2) worldInverse);
    texcoord += float2(mask, -mask) * offsetL;

    左:视差贴图效果;右:基于物理的折射效果。

    当光线从侧面射进眼球时,经过折射和透射后,会在另一侧发生较强烈的透射光环:

    这种跟光线角度相关的折射,可以通过预计算的方式解决:

  • 参合多介质渲染(participating media rendering)。它在近年来广泛地被应用在体积光、云彩和天空相关的渲染技术中。更多内容请参看:Rendering participating media

    利用participating media rendering技术渲染的体积雾。

3.2.3 瞳孔的缩放

瞳孔的放大和缩小实现非常简单,通过控制采样瞳孔贴图的UV即可。

UE4的眼球模型的UV布局

Mike的眼球材质提供了缩放参数,以便调节瞳孔大小。

3.2.4 虹膜的颜色

虹膜的颜色可以首先给定一个虹膜纹理的灰度图,然后用给定虹膜颜色乘以灰度颜色,即可得到最终虹膜的颜色,这样可以通过一套资源来实现不同颜色的眼球的渲染。

Mike的眼球材质提供了更改瞳孔、虹膜等颜色的参数。

3.2.5 其它眼球细节

眼球的细节刻画可以增加其真实度,使画面更上一个台阶。

  • 不平坦反射。真实的眼白不是完全镜面平坦的,有一定程度的凹凸不平,可以通过类Sine函数扰动其法线贴图达到模拟效果。

  • 湿润度。大多数人的眼睛都带有不同程度的泪水,具有不同的湿润度。可通过建立一层透明网格来模拟此效果。

    不同湿润度的网格模型

    模拟出来的效果如下:

    眼球的湿润度从左到右:低、中、高。

    此外,可以模糊湿润网格,以便更好地将眼睛边缘做融合:

  • 眼睛自反射。由于眼球具体较强的反射,而已睫毛、眼皮会反射在上面,如果这部分被忽略,将会有点怪。

    左:没有自反射;右:有睫毛、眼皮等的自反射。

    然而要实时地计算自反射会消耗较多的性能,可预先烘焙环境遮蔽图,渲染时直接采样:

  • 瞳孔、虹膜、巩膜等部位之间的过渡。由于它们分属不同的材质,有着各自的属性,如果它们的交界处不进行插值过渡,将会出现恐怖的效果(下图右)。

    左:采用了过渡;右:未采用过渡。

    过渡曲线可采用类似Sine函数的变种:

  • 血色和血丝。血丝可在眼白的纹理添加血管纹理细节,而血色可在计算时乘以由一张遮罩纹理控制的红色来模拟。

    带血丝细节的眼球纹理。

  • 接触阴影(Contact Shadow)。半透明材质可以启用接触阴影。此功能使用类似于光源接触阴影的功能,但不会链接到光源接触阴影参数。这是屏幕空间效果,可以作为几何体的补充,也可以取代几何体,让眼睛看起来牢牢地长在眼眶中,提高可信度。

    左:未开启接触阴影;右:开启接触阴影,开启后,反射光变弱了。

3.3 眼球的底层实现

本节将深入源码层剖析UE的眼睛渲染细节。需要注意的是,要将眼睛材质的Shading Model选择为Eye(下图),并且眼睛着色模式启用了次表面散射,即眼睛着色模式是一种特殊化的次表面剖面(Subsurface Profile)着色模式。

在shader层,Eye的渲染模型跟普通的PBR流程和逻辑区别甚微,跟它相关的代码文件:

  • G:\UnrealEngine\Engine\Shaders\Private\DeferredLightingCommon.ush。
  • G:\UnrealEngine\Engine\Shaders\Private\BasePassPixelShader.usf。
  • G:\UnrealEngine\Engine\Shaders\Private\ShadowProjectionPixelShader.usf。
  • G:\UnrealEngine\Engine\Shaders\Private\ShadingModelsMaterial.ush。

首先分析ShadingModelsMaterial.ush在眼睛着色模式下GBuffer数据初始化相关的代码:

void SetGBufferForShadingModel(
    in out FGBufferData GBuffer, 
    in const FMaterialPixelParameters MaterialParameters,
    const float Opacity,
    const half3 BaseColor,
    const half  Metallic,
    const half  Specular,
    const float Roughness,
    const float3 SubsurfaceColor,
    const float SubsurfaceProfile,
    const float dither)
{

    // ... (省略部分代码)

#elif MATERIAL_SHADINGMODEL_EYE
    GBuffer.ShadingModelID = SHADINGMODELID_EYE;
    GBuffer.CustomData.x = EncodeSubsurfaceProfile(SubsurfaceProfile).x;
    GBuffer.CustomData.w = 1.0f - saturate(GetMaterialCustomData0(MaterialParameters)); // Opacity = 1.0 - Iris Mask
    GBuffer.Metallic = saturate(GetMaterialCustomData1(MaterialParameters));            // Iris Distance

// 如果定义了虹膜法线,进入了一段较复杂的数据处理。可见开启虹膜法线需要消耗较多性能。
#if IRIS_NORMAL
    float IrisMask      = saturate( GetMaterialCustomData0(MaterialParameters) );
    float IrisDistance  = saturate( GetMaterialCustomData1(MaterialParameters) );

    GBuffer.CustomData.x = EncodeSubsurfaceProfile(SubsurfaceProfile).x;
    GBuffer.CustomData.w = 1.0 - IrisMask;  // Opacity

    float2 WorldNormalOct = UnitVectorToOctahedron( GBuffer.WorldNormal );

    // CausticNormal stored as octahedron
    #if NUM_MATERIAL_OUTPUTS_GETTANGENTOUTPUT > 0
        // 通过法线的变换,创建一些凹陷度。
        // Blend in the negative intersection normal to create some concavity
        // Not great as it ties the concavity to the convexity of the cornea surface
        // No good justification for that. On the other hand, if we're just looking to
        // introduce some concavity, this does the job.
        float3 PlaneNormal = normalize( GetTangentOutput0(MaterialParameters) );
        float3 CausticNormal = normalize( lerp( PlaneNormal, -GBuffer.WorldNormal, IrisMask*IrisDistance ) );
        float2 CausticNormalOct  = UnitVectorToOctahedron( CausticNormal );
        float2 CausticNormalDelta = ( CausticNormalOct - WorldNormalOct ) * 0.5 + (128.0/255.0);
        GBuffer.Metallic = CausticNormalDelta.x;
        GBuffer.Specular = CausticNormalDelta.y;
    #else
        float3 PlaneNormal = GBuffer.WorldNormal;
        GBuffer.Metallic = 128.0/255.0;
        GBuffer.Specular = 128.0/255.0;
    #endif

    // IrisNormal CustomData.yz
    #if NUM_MATERIAL_OUTPUTS_CLEARCOATBOTTOMNORMAL > 0
        float3 IrisNormal = normalize( ClearCoatBottomNormal0(MaterialParameters) );
        #if MATERIAL_TANGENTSPACENORMAL
            IrisNormal = normalize( TransformTangentVectorToWorld( MaterialParameters.TangentToWorld, IrisNormal ) );
        #endif
    #else
        float3 IrisNormal = PlaneNormal;
    #endif

    float2 IrisNormalOct  = UnitVectorToOctahedron( IrisNormal );
    float2 IrisNormalDelta = ( IrisNormalOct - WorldNormalOct ) * 0.5 + (128.0/255.0);
    GBuffer.CustomData.yz = IrisNormalDelta;
#else
    GBuffer.Metallic = saturate(GetMaterialCustomData1(MaterialParameters));            // Iris Distance

    #if NUM_MATERIAL_OUTPUTS_GETTANGENTOUTPUT > 0
        float3 Tangent = GetTangentOutput0(MaterialParameters);
        GBuffer.CustomData.yz = UnitVectorToOctahedron( normalize(Tangent) ) * 0.5 + 0.5;
    #endif
#endif

    // ... (省略部分代码)
    
}

接着分析接触阴影相关的代码,在DeferredLightingCommon.ush内:

void GetShadowTerms(FGBufferData GBuffer, FDeferredLightData LightData, float3 WorldPosition, float3 L, float4 LightAttenuation, float Dither, inout FShadowTerms Shadow)
{
    // 默认接触阴影强度是0。
    float ContactShadowLength = 0.0f;
    // 接触阴影长度屏幕空间缩放
    const float ContactShadowLengthScreenScale = View.ClipToView[1][1] * GBuffer.Depth;

    BRANCH
    if (LightData.ShadowedBits)
    {

        // ... (省略部分代码)
        
        // 根据缩放因子计算接触阴影长度。
        FLATTEN
        if (LightData.ShadowedBits > 1 && LightData.ContactShadowLength > 0)
        {
            ContactShadowLength = LightData.ContactShadowLength * (LightData.ContactShadowLengthInWS ? 1.0f : ContactShadowLengthScreenScale);
        }
    }

#if SUPPORT_CONTACT_SHADOWS
    // 如果是头发或者眼睛着色模式,接触阴影长度强制缩放到0.2倍(这个值应该是测量过的值)。
    if ((LightData.ShadowedBits < 2 && (GBuffer.ShadingModelID == SHADINGMODELID_HAIR))
        || GBuffer.ShadingModelID == SHADINGMODELID_EYE)
    {
        ContactShadowLength = 0.2 * ContactShadowLengthScreenScale;
    }

    #if MATERIAL_CONTACT_SHADOWS
        ContactShadowLength = 0.2 * ContactShadowLengthScreenScale;
    #endif

    BRANCH
    if (ContactShadowLength > 0.0)
    {
        float StepOffset = Dither - 0.5;
        // 计算接触阴影
        float ContactShadow = ShadowRayCast( WorldPosition + View.PreViewTranslation, L, ContactShadowLength, 8, StepOffset );
        
        Shadow.SurfaceShadow *= ContactShadow;
        
        // 计算透射阴影
        FLATTEN
        if( GBuffer.ShadingModelID == SHADINGMODELID_HAIR )
            Shadow.TransmissionShadow *= ContactShadow;
        // 如果是眼睛渲染模式,则不加深阴影强度,否正加深。
        else if( GBuffer.ShadingModelID != SHADINGMODELID_EYE )
            Shadow.TransmissionShadow *= ContactShadow * 0.5 + 0.5;
    }
#endif
}

还有小部分逻辑在ShadowProjectionPixelShader.ush,关于阴影计算的:

void Main(
    in float4 SVPos : SV_POSITION,
    out float4 OutColor : SV_Target0
    )
{
    // ... (省略部分代码)

        if (IsSubsurfaceModel(GBufferData.ShadingModelID))
        {
            float Opacity = GBufferData.CustomData.a;
            // Derive density from a heuristic using opacity, tweaked for useful falloff ranges and to give a linear depth falloff with opacity
            float Density = -.05f * log(1 - min(Opacity, .999f));
            // 如果是头发或眼睛渲染模式,不透明度和密度强制设为1。
            if( GBufferData.ShadingModelID == SHADINGMODELID_HAIR || GBufferData.ShadingModelID == SHADINGMODELID_EYE )
            {
                Opacity = 1;
                Density = 1;
            }
        
    // ... (省略部分代码)
}

从上面分析可知,眼睛着色模式与次表面剖面着色模式基本一致,只是在GBuffer数据初始化、阴影计算上有所差别。

3.4 眼球的材质

本节将分析Mike的眼球主材质和附属物材质。

3.4.1 眼球主材质

眼球主材质是M_EyeRefractive,下图是眼球主材质的总览图,节点排布有点乱(UE材质编辑器并没有提供自动排布功能)。下面将分小节重点分析眼球材质的重要或主要算法过程,其它的小细节将被忽略。

3.4.1.1 眼球的折射

如上图所示,眼球的折射主要通过材质函数ML_EyeRefraction实现,下面将对它的输入参数和输出参数进行分析。

材质函数ML_EyeRefraction的输入参数:

  • InternalIoR:眼球内部折射,用于模拟光线进入虹膜后的折射率,数值通常在\([1.0,1.4]\)之间,越大折射效果越明显。直接由变量IoR提供。

  • ScaleByCenter:眼球(包含眼白、瞳孔、虹膜等)的缩放大小。直接由变量ScaleByCenter提供。

  • LimbusUVWidth:角膜缘的UV宽度,由LimbusUVWidthColorLimbusUVWidthShading组成的2D向量提供。

  • DepthScale:虹膜的深度缩放。数值越大,折射效果越明显。由变量DepthScale提供。

  • DepthPlaneOffset:深度平面偏移。决定瞳孔的大小和深度。由变量Iris UV RadiusScaleByCenter共同算出UV,然后采样贴图T_EyeMidPlaneDisplacement的R通道提供数据。

  • MidPlaneDisplacement:中平面偏移,决定角膜平面到瞳孔平面的深度偏移,瞳孔周边的偏移会较小。直接采样贴图T_EyeMidPlaneDisplacement获得。T_EyeMidPlaneDisplacement如下:

  • EyeDirectionWorld:眼球模型的世界空间的法线。由UseEyeBuldge控制的两张法线贴图T_Eye_NT_Eye_Sphere_N采样后,由切线空间变换到世界空间获得。其中T_Eye_N是中间有凸出的眼球结构(下图左),而T_Eye_Sphere_N则没有(下图右):

  • IrisUVRadius:虹膜UV半径,直接由变量Iris UV Radius提供。

材质函数ML_EyeRefraction的输出参数:

  • RefractedUV:折射后的UV,经过材质函数内部计算后,输出的UV结果,后面可以用于采样漫反射、其它遮罩贴图。
  • Transparency:虹膜颜色透明度。
  • IrisMask:标识虹膜UV区域的遮罩。后续用于虹膜区域的相关着色处理。

上面只是分析了ML_EyeRefraction的输入、输出参数,下面将进入其内部计算过程:

首先分析折射向量(Refraction Direction)的计算:

float airIoR = 1.00029;

// 空气对眼球内部的折射率比。
float n = airIoR / internalIoR;
// 法线和摄像机向量的夹角相关的缩放因子
float facing = dot(normalW, cameraW);
// 视角缩放后的折射率比。
float w = n * facing;
// 根据n和w计算中间因子。
float k = sqrt(1+(w-n)*(w+n));

// 根据n、w和k算出最终的折射向量。
float3 t;
t = (w - k)*normalW - n*cameraW;
t = normalize(t);
return -t;

再分析折射纹理偏移(Refracted UV Offset)的计算:

由上图可见,要算出右边红色方框标识的折射纹理偏移,需要用到众多输入参数,以及经过多次坐标运算和角度计算。虽然过程比较复杂,但原理跟3.2.2 瞳孔的次表面散射的基于物理的折射一致。

有了折射向量和折射纹理偏移,就可以通过数次基本运算调整,算出最终的输出参数RefractedUV

对于输出参数IrisMask的计算,由以下shader代码完成:

// 计算Iris遮罩(R通道)和角膜缘过渡区域(G通道)
UV = UV - float2(0.5f, 0.5f);

float2 m, r;
r = (length(UV) - (IrisUVRadius - LimbusUVWidth)) / LimbusUVWidth;
m = saturate(1 - r);
// 通过类sine函数变种,输出柔和的混合因子,使得角膜缘过渡自然、柔和。
m = smoothstep(0, 1, m);
return m;

3.4.1.2 瞳孔的缩放

由上图可以看出,如果开启了折射(Refraction On/Off为true),则会使用上一小节计算的折射后的UV坐标,经过坐标换算和中心缩放,成为Custom shader节点的输入参数,它的输入还有PupilScale,用于决定瞳孔的大小。Custom shader节点的代码如下:

// 主要是将UV坐标绕着纹理中心进行PupilScale缩放

// float2 UV, float PupilScale

float2 UVcentered = UV - float2(0.5f, 0.5f);
float UVlength = length(UVcentered);
// UV on circle at distance 0.5 from the center, in direction of original UV
float2 UVmax = normalize(UVcentered)*0.5f;

float2 UVscaled = lerp(UVmax, float2(0.f, 0.f), saturate((1.f - UVlength*2.f)*PupilScale));
return UVscaled + float2(0.5f, 0.5f);

3.4.1.3 眼球颜色的混合

眼球的颜色主要有两种颜色提供:

  • 眼白颜色(Sclera Color):由T_EyeScleraBaseColor采样获得,并且经过变量ScleraBrightness缩放。其中采样的UV没有折射,只经过中心点缩放。
  • 虹膜颜色(Iris Color):颜色采样T_EyeIrisBaseColor获得,并且纹理UV经过3.4.1.1 眼球的折射的折射计算,以及3.4.1.2 瞳孔的缩放的中心点缩放。采样得到的颜色经过IrisBRightness和角膜缘(Limbus)相关的参数缩放。

以上两种颜色经过ML_EyeRefraction输出的IrisMask进行插值,添加虹膜颜色(CloudyIris)后,最终输出到Base Color引脚。

3.4.1.4 眼球的法线

眼球法线的UV经过中心点缩放,接着去采用法线贴图T_Eye_Wet_N,得出的法线经过材质函数FlattenNormal和缩放因子调整法线强度,最终输出到法线引脚。

其中FlattenNormal的强度由ML_EyeRefraction输出的IrisMask指示的虹膜区域在\([FlattenNormal, 1.0]\)进行插值。如果是虹膜区域,则不受法线影响,即完全光滑的。

3.4.1.5 虹膜的遮罩和深度

虹膜的遮罩直接由ML_EyeRefraction输出的IrisMask获得。

虹膜的深度由折射后的纹理UV计算出距离圆心(0.5,0.5)的长度,获得与Iris UV Radius的比值,再经过Iris Concavity Scale缩放和Power调整后,得到最终结果。(下图)

3.4.1.6 清漆底部法线(ClearCoatBottomNormal)

如上图,Custom节点与3.4.1.2 瞳孔的缩放中的一样,计算了UV沿着中心点缩放,接着去采样瞳孔法线纹理iris08_leftEye_nml,获得的结果经过IrisDispStrength控制的因子缩放,最后通过节点BlendAngleCorrectedNormals与眼球表面法线混合,输出结果到Output节点ClearCoatBottomNormal

3.4.1.7 眼球的其它部分

眼球的其它属性,如镜面度、粗糙度、切线等,都比较简单,直接看材质即可明白其计算过程,故这里不做分析。

3.4.2 眼球附属物材质

上小节分析了眼球的主材质,然而,眼睛的渲染还包含了很多附加物体,它们各自有着独立的材质属性(下图)。

3.4.2.1 泪腺液体

泪腺几何体是一个包围着眼皮周围的网格体(上图),提供了眼皮处的高光反射(下图),用于模拟光线照射到泪腺后的镜面反射。

左:无泪腺几何体;右:有泪腺几何体

它的材质如下图,采用了透明混合模式:

它的颜色、金属度默认都是1,可见用高反射率和高金属度来获得极强的镜面反射效果。

它的粗糙度计算较复杂,如下图:

纹理坐标经过变量DetailScale_1缩放后,去采样细节纹理skin_h,获得的结果再依次经过DetailAmount缩放、固定常量0.1和Roughness调整后,进入自定义shader节点CurveToRoughness计算,最终得到结果。其中CurveToRoughness的shader代码如下:

// Specular antialiasing using derivatives and normal variance

float3 N = WorldNormal;
float3 dN = fwidth( N );
float Curvature = sqrt( 1 - dot( normalize( N + dN ), N ) );

// TODO find an approximation that more directly uses Roughness
float Power = 2 / pow( Roughness, 4 ) - 2;
float Angle = 4.11893 / sqrt( Power ) + Curvature;
Power = 16.9656 / ( Angle * Angle );
Roughness = sqrt( sqrt( 2 / (Power + 2) ) );

return Roughness;

上面涉及的粗糙度算法在Rock-Solid Shading: Image Stability Without Sacrificing Detail有详细描述。

它的法线计算比较简单,采样法线贴图skin_n后经过变量DetailAmount调整,就得到最终结果。

此外,它还增加了世界坐标偏移,由变量'DepthOffset'控制偏移量,经过材质函数CameraOffset得到相机空间的偏移。

3.4.2.2 遮蔽模糊体

遮蔽模糊体跟泪腺液体类似,环绕于眼角周边,用于遮挡部分光照并模糊,使得周边混合更真实(下图)。

左:无遮蔽模糊体;右:有遮蔽模糊体

它的材质采用透明混合模式,并且光照模型是Unlit。它的总览图如下:

可分下面几个部分进行分析:

  • 不透明度(Opacity):

    这部分主要是生成需要遮蔽和模糊的区域掩码。过程大致是通过采样初始掩码图,加上纹理线性过渡、反向、加上Power运算调整,以及若干变量控制的因子进行基本运算,获得眼部周边掩码(下图)。

  • 模糊(Blur):

    在当前UV周边采样16个Scene Color求得平均值。此处的Scene Color一定是已经渲染眼球后的颜色,因为眼球是非透明物体,可保证在透明的遮蔽模糊体之前先绘制。

  • 颜色(Color):

    原始颜色的输出很简单,利用上面计算的遮罩,在白色和Blur Color之间插值,然后与上面模糊后的场景颜色相乘。

  • 阴影(Shadow):

    如上图,通过UV的上下左右线性渐变及调整后获得4个不同的值,进行相乘,获得周边黑色,最后通过变量在1.0之间插值,获得顶部为深色的阴影图。

  • 综合计算:

    在此阶段,利用上面的几个计算结果,颜色和阴影相乘,并预乘了Alpha,获得最终颜色和不透明度。

此外,还有位置偏移的计算,这里将忽略。

3.4.2.3 眼角混合体

眼角混合体为眼角增加血色及血丝细节,并调整亮度,使得眼白过渡更自然(下图)。

左:无眼角混合体;右:有眼角混合体

它的材质启用了次表面散射,并且混合模式是裁剪(Masked),材质总览图如下:

下面将其拆分成若干部分进行分析:

  • 渐变掩码:

    利用UV的横坐标获得线性过渡,用Power调整强度,然后用SmoothStep获得平滑过渡的掩码图。

  • 颜色:

    通过几个变量将UV坐标进行拉伸,去采样眼睛贴图eye_sclera_right_clr,获得拉伸后的颜色,经过眼白亮度调整和由上节计算出的掩码决定的眼白到血色的调整,获得最终颜色。其中眼角偏红,呈现出更多血色,而靠近瞳孔的区域受影响程度较低。

  • 法线:

    法线的获得,主要由上面计算出的掩码,在向量[0, 0, 1]和[-1, 0, 0]插值获得。

3.4.2.4 睫毛和眉毛

由于睫毛和眉毛的材质属于Hair着色模式,虽然是眼睛的组成部分,但其实是毛发渲染的范畴,后续章节将会详细阐述。

3.5 眼球渲染总结

由上面可知,虽然眼睛的渲染技术不如皮肤渲染来得更高深、更系统,但由于其涉及的部位和细节多,环环相扣,各个材质之间相辅相成,形成了一套完整而逼真的眼睛渲染体系。

本章结尾,引用官方文档的建议:

在开发数字人类角色时,我们在模型中使用了一些不同方法和材质提升了角色眼部的逼真度。如上所述,许多眼部设置与材质设置和采集的参考资料之间存在着相互依赖的关系。我们强烈建议使用我们的眼部设置作为您的起点。

可见,要完全从零开始制作一个成像逼真的眼睛的资源(模型、贴图、材质等),还是有相当的难度。幸好慷慨的虚幻引擎官方已经给出了足够多的示例及资源,以供个人及团队研究和研发,大大缩短了学习、开发的周期。

本系列文章其它部分

特别说明

  • 感谢参考文献的所有作者们!
  • 后续还有毛发渲染等部分,敬请期待!

参考文献

posted @ 2019-07-07 03:50 0向往0 阅读(...) 评论(...) 编辑 收藏