剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

0嚮往0發表於2019-07-07

三、眼球渲染

都說眼睛是人類心靈的窗戶,若是眼睛渲染得逼真,將給虛擬角色點睛之筆,給予其栩栩如生的靈魂。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

Mike那深邃的眼眸,唏噓的鬍渣子,神乎其神的眼神。。。應該征服了不少迷妹

再來一張超近距離的特寫:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

超近距離的眼睛特寫,細節刻畫得無與倫比,足以以假亂真。

然而,要渲染出如此逼真有神的眼睛,可不是那麼簡單,需要經過多道工序,運用許多渲染技法,刻畫很多細節。

3.1 眼球的構造及理論

3.1.1 眼球的構造

生物學的眼球解剖圖非常複雜,涉及的部位數十種。(下圖)

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

人類眼球的生物學剖面圖,涉及部位多達數十種。

在圖形渲染領域,當然不可能關注這麼多細節,可以將眼球構造做簡化,只關注其中的幾個部位:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

上圖所示的序號代表的部位:

  • 1 - 鞏膜(sclera):也稱為“眼白”,通常非常溼潤,包含少量的觸感紋理、血絲等細節。
  • 2 - 角膜緣(limbus):角膜緣存在於虹膜和鞏膜之間的深色環形。有些眼睛中的角膜緣更為明顯,從側面看時往往會消退。
  • 3 - 虹膜(iris):虹膜是圍繞在眼睛中心周圍的一圈色環。如果某個人有“綠”眼睛,就是因為虹膜主要是綠色的。在真實的眼睛中,虹膜是類似肌肉的纖維結構,有擴張和收縮功能,以讓更多光線進入瞳孔或者不讓光線進入瞳孔。還需要注意的是,在真實世界中,虹膜實際上更像是圓盤或錐形,不會向眼部其餘部分突出。
  • 4 - 瞳孔(pupil):瞳孔是眼睛中心的黑點。這是一個孔,光線穿過這個孔後才會被視網膜的視杆和視錐捕捉到。
  • 5 - 角膜(cornea):角膜是位於虹膜表面上的一層透明的、充滿液體的圓頂結構。

3.1.2 眼球的渲染理論

由於眼球充滿了液體,因此會折射照射進來的任何光線。在真實世界中從多個角度觀察眼球時就會看到這種效果。虹膜和瞳孔會因為折射而變形,因為它們是透過角膜觀看的。

遊戲和電影中用來解決這個問題的傳統方法是建立兩層獨立的眼睛表面,一層提供鞏膜、虹膜和瞳孔,另一層位於頂部,提供角膜和眼睛的總體溼潤度。這樣底層表面透過溼潤層觀看時就會產生折射。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

《A Boy and His Kite》中的男孩眼睛中採用的就是兩層表面的渲染模型

根據上面的分析,以及簡化後的眼球解剖結構,就可以得出結論,要渲染好眼睛,需要著重實現的效果包括:

  • 角膜的半透和光澤反射效果。
  • 瞳孔的次表面散射。
  • 瞳孔的縮放。最好根據整個場景的光照強度動態調整縮放大小。
  • 虹膜的顏色變化。
  • 其它眼球細節。

下節將詳細探討。

3.2 眼球的渲染技術

本節主要參考來源:

3.2.1 角膜的半透和光澤反射

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

簡單的做法就是直接把角膜看做一個半透明光澤球體的反射,正常的做法是用PBR流程計算其鏡面反射和IBL反射,然後給眼球一張虹膜和眼白的貼圖,這張貼圖作為角膜下面的折射效果,最後給角膜設定一個混合係數,把光澤球體反射效果和虹膜及眼白貼圖上的顏色進行混合。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

角膜的鏡面反射和環境反射豐富了眼球的細節,增加了真實可信度

3.2.2 瞳孔的次表面散射

瞳孔本身實際上也是一個高低不平有縱深感的結構,它與角膜存在一定距離。這使得瞳孔會發生折射,並且,當光線到達瞳孔表面的時候,還會進一步在瞳孔結構內部發生次表面散射。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

把眼球看成了一個雙層結構,外面一層是角膜,裡面一層是瞳孔的表面,而角膜和瞳孔之間我們可以認為是充斥了某種透明液體。

光線在進入瞳孔組織的內部前,首先會在角膜的表面發生一次折射,然後進入瞳孔組織的內部,產生散射,最後從瞳孔表面的另一個點散射出來。這裡就涉及到了兩個問題:

(1)一束射到角膜表面的光線在經過折射後,如何計算最終入射到瞳孔表面的位置;

(2)光線進入角膜內部後,如何計算其散射效果。

為了解決以上兩個問題,可使用次表面紋理對映(Subsurface texture mapping),這個方法旨在解決多層厚度不均勻的材質的次表面散射效果的計算。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

如上圖,每一層材質都有一個單獨的深度圖,儲存在一個通道里,然後每一層單獨的材質被認為是均勻的,擁有相同的散射、吸收係數以及相應的相位函式(散射相關的引數)。然後,以視線和第一層材質的交點為起點,沿著視線方向對多層材質進行ray-marching,每行進一步就根據位置和深度圖計算當前點位於材質的哪一層,對應什麼散射引數,再根據上一步的位置以及光照方向計算散射和吸收,直到ray-marching結束。具體到眼球的散射計算,實際上只有一層散射材質,即瞳孔材質。因此我們只需要提供瞳孔表面的深度圖,並設定好瞳孔材質的相關散射引數,再結合次表面紋理對映的方法計算即可。

這部分主要涉及的渲染技術:

  • 視差貼圖(parallax mapping,也叫relief mapping)。可以通過ray marching的方法結合一張深度圖在相對平坦的幾何表面上實現視覺正確的高低起伏效果,法線效果雖然也能在平面上產生凹凸起伏,但在比較斜的視角下平面還是平面,視差貼圖則不會這樣。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    左:normal mapping效果;右:parallax mapping效果。可見在傾斜視角下,後者效果要好很多。

  • 基於物理的折射(Physically based Refraction)。與視差貼圖的欺騙式計算不同,基於物理的折射是根據真實的折射模型進行模擬,效果更真實。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

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

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    左:視差貼圖效果;右:基於物理的折射效果。

    當光線從側面射進眼球時,經過折射和透射後,會在另一側發生較強烈的透射光環:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    這種跟光線角度相關的折射,可以通過預計算的方式解決:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • 參合多介質渲染(participating media rendering)。它在近年來廣泛地被應用在體積光、雲彩和天空相關的渲染技術中。更多內容請參看:Rendering participating media

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    利用participating media rendering技術渲染的體積霧。

3.2.3 瞳孔的縮放

瞳孔的放大和縮小實現非常簡單,通過控制取樣瞳孔貼圖的UV即可。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

UE4的眼球模型的UV佈局

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

Mike的眼球材質提供了縮放引數,以便調節瞳孔大小。

3.2.4 虹膜的顏色

虹膜的顏色可以首先給定一個虹膜紋理的灰度圖,然後用給定虹膜顏色乘以灰度顏色,即可得到最終虹膜的顏色,這樣可以通過一套資源來實現不同顏色的眼球的渲染。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

Mike的眼球材質提供了更改瞳孔、虹膜等顏色的引數。

3.2.5 其它眼球細節

眼球的細節刻畫可以增加其真實度,使畫面更上一個臺階。

  • 不平坦反射。真實的眼白不是完全鏡面平坦的,有一定程度的凹凸不平,可以通過類Sine函式擾動其法線貼圖達到模擬效果。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • 溼潤度。大多數人的眼睛都帶有不同程度的淚水,具有不同的溼潤度。可通過建立一層透明網格來模擬此效果。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    不同溼潤度的網格模型

    模擬出來的效果如下:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    眼球的溼潤度從左到右:低、中、高。

    此外,可以模糊溼潤網格,以便更好地將眼睛邊緣做融合:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • 眼睛自反射。由於眼球具體較強的反射,而已睫毛、眼皮會反射在上面,如果這部分被忽略,將會有點怪。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

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

    然而要實時地計算自反射會消耗較多的效能,可預先烘焙環境遮蔽圖,渲染時直接取樣:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • 瞳孔、虹膜、鞏膜等部位之間的過渡。由於它們分屬不同的材質,有著各自的屬性,如果它們的交界處不進行插值過渡,將會出現恐怖的效果(下圖右)。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    左:採用了過渡;右:未採用過渡。

    過渡曲線可採用類似Sine函式的變種:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • 血色和血絲。血絲可在眼白的紋理新增血管紋理細節,而血色可在計算時乘以由一張遮罩紋理控制的紅色來模擬。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    帶血絲細節的眼球紋理。

  • 接觸陰影(Contact Shadow)。半透明材質可以啟用接觸陰影。此功能使用類似於光源接觸陰影的功能,但不會連結到光源接觸陰影引數。這是螢幕空間效果,可以作為幾何體的補充,也可以取代幾何體,讓眼睛看起來牢牢地長在眼眶中,提高可信度。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    左:未開啟接觸陰影;右:開啟接觸陰影,開啟後,反射光變弱了。

3.3 眼球的底層實現

本節將深入原始碼層剖析UE的眼睛渲染細節。需要注意的是,要將眼睛材質的Shading Model選擇為Eye(下圖),並且眼睛著色模式啟用了次表面散射,即眼睛著色模式是一種特殊化的次表面剖面(Subsurface Profile)著色模式。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

在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材質編輯器並沒有提供自動排布功能)。下面將分小節重點分析眼球材質的重要或主要演算法過程,其它的小細節將被忽略。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

3.4.1.1 眼球的折射

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

如上圖所示,眼球的折射主要通過材質函式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如下:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • EyeDirectionWorld:眼球模型的世界空間的法線。由UseEyeBuldge控制的兩張法線貼圖T_Eye_NT_Eye_Sphere_N取樣後,由切線空間變換到世界空間獲得。其中T_Eye_N是中間有凸出的眼球結構(下圖左),而T_Eye_Sphere_N則沒有(下圖右):

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • IrisUVRadius:虹膜UV半徑,直接由變數Iris UV Radius提供。

材質函式ML_EyeRefraction的輸出引數:

  • RefractedUV:折射後的UV,經過材質函式內部計算後,輸出的UV結果,後面可以用於取樣漫反射、其它遮罩貼圖。
  • Transparency:虹膜顏色透明度。
  • IrisMask:標識虹膜UV區域的遮罩。後續用於虹膜區域的相關著色處理。

上面只是分析了ML_EyeRefraction的輸入、輸出引數,下面將進入其內部計算過程:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

首先分析折射向量(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)的計算:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

由上圖可見,要算出右邊紅色方框標識的折射紋理偏移,需要用到眾多輸入引數,以及經過多次座標運算和角度計算。雖然過程比較複雜,但原理跟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 瞳孔的縮放

剖析Unreal Engine超真實人類的渲染技術Part 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 眼球顏色的混合

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

眼球的顏色主要有兩種顏色提供:

  • 眼白顏色(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 眼球的法線

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

眼球法線的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調整後,得到最終結果。(下圖)

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

3.4.1.6 清漆底部法線(ClearCoatBottomNormal)

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

如上圖,Custom節點與3.4.1.2 瞳孔的縮放中的一樣,計算了UV沿著中心點縮放,接著去取樣瞳孔法線紋理iris08_leftEye_nml,獲得的結果經過IrisDispStrength控制的因子縮放,最後通過節點BlendAngleCorrectedNormals與眼球表面法線混合,輸出結果到Output節點ClearCoatBottomNormal

3.4.1.7 眼球的其它部分

眼球的其它屬性,如鏡面度、粗糙度、切線等,都比較簡單,直接看材質即可明白其計算過程,故這裡不做分析。

3.4.2 眼球附屬物材質

上小節分析了眼球的主材質,然而,眼睛的渲染還包含了很多附加物體,它們各自有著獨立的材質屬性(下圖)。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

3.4.2.1 淚腺液體

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

淚腺幾何體是一個包圍著眼皮周圍的網格體(上圖),提供了眼皮處的高光反射(下圖),用於模擬光線照射到淚腺後的鏡面反射。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

左:無淚腺幾何體;右:有淚腺幾何體

它的材質如下圖,採用了透明混合模式:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

它的顏色、金屬度預設都是1,可見用高反射率和高金屬度來獲得極強的鏡面反射效果。

它的粗糙度計算較複雜,如下圖:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

紋理座標經過變數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 遮蔽模糊體

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

遮蔽模糊體跟淚腺液體類似,環繞於眼角周邊,用於遮擋部分光照並模糊,使得周邊混合更真實(下圖)。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

左:無遮蔽模糊體;右:有遮蔽模糊體

它的材質採用透明混合模式,並且光照模型是Unlit。它的總覽圖如下:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

可分下面幾個部分進行分析:

  • 不透明度(Opacity):

    這部分主要是生成需要遮蔽和模糊的區域掩碼。過程大致是通過取樣初始掩碼圖,加上紋理線性過渡、反向、加上Power運算調整,以及若干變數控制的因子進行基本運算,獲得眼部周邊掩碼(下圖)。

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

  • 模糊(Blur):

    在當前UV周邊取樣16個Scene Color求得平均值。此處的Scene Color一定是已經渲染眼球后的顏色,因為眼球是非透明物體,可保證在透明的遮蔽模糊體之前先繪製。

  • 顏色(Color):

    原始顏色的輸出很簡單,利用上面計算的遮罩,在白色和Blur Color之間插值,然後與上面模糊後的場景顏色相乘。

  • 陰影(Shadow):

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    如上圖,通過UV的上下左右線性漸變及調整後獲得4個不同的值,進行相乘,獲得周邊黑色,最後通過變數在1.0之間插值,獲得頂部為深色的陰影圖。

  • 綜合計算:

    在此階段,利用上面的幾個計算結果,顏色和陰影相乘,並預乘了Alpha,獲得最終顏色和不透明度。

此外,還有位置偏移的計算,這裡將忽略。

3.4.2.3 眼角混合體

眼角混合體為眼角增加血色及血絲細節,並調整亮度,使得眼白過渡更自然(下圖)。

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

左:無眼角混合體;右:有眼角混合體

它的材質啟用了次表面散射,並且混合模式是裁剪(Masked),材質總覽圖如下:

剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

下面將其拆分成若干部分進行分析:

  • 漸變掩碼:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    利用UV的橫座標獲得線性過渡,用Power調整強度,然後用SmoothStep獲得平滑過渡的掩碼圖。

  • 顏色:

    剖析Unreal Engine超真實人類的渲染技術Part 2 - 眼球渲染

    通過幾個變數將UV座標進行拉伸,去取樣眼睛貼圖eye_sclera_right_clr,獲得拉伸後的顏色,經過眼白亮度調整和由上節計算出的掩碼決定的眼白到血色的調整,獲得最終顏色。其中眼角偏紅,呈現出更多血色,而靠近瞳孔的區域受影響程度較低。

  • 法線:

    法線的獲得,主要由上面計算出的掩碼,在向量[0, 0, 1]和[-1, 0, 0]插值獲得。

3.4.2.4 睫毛和眉毛

由於睫毛和眉毛的材質屬於Hair著色模式,雖然是眼睛的組成部分,但其實是毛髮渲染的範疇,後續章節將會詳細闡述。

3.5 眼球渲染總結

由上面可知,雖然眼睛的渲染技術不如皮膚渲染來得更高深、更系統,但由於其涉及的部位和細節多,環環相扣,各個材質之間相輔相成,形成了一套完整而逼真的眼睛渲染體系。

本章結尾,引用官方文件的建議:

在開發數字人類角色時,我們在模型中使用了一些不同方法和材質提升了角色眼部的逼真度。如上所述,許多眼部設定與材質設定和採集的參考資料之間存在著相互依賴的關係。我們強烈建議使用我們的眼部設定作為您的起點。

可見,要完全從零開始製作一個成像逼真的眼睛的資源(模型、貼圖、材質等),還是有相當的難度。幸好慷慨的虛幻引擎官方已經給出了足夠多的示例及資源,以供個人及團隊研究和研發,大大縮短了學習、開發的週期。

特別說明

  • 感謝參考文獻的所有作者們!
  • 後續還有毛髮渲染等部分,敬請期待!

參考文獻

相關文章