[图形学] Real Shading in Unreal Engine 4

Posted ZJU_fish1996

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[图形学] Real Shading in Unreal Engine 4相关的知识,希望对你有一定的参考价值。

                https://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf

图1 UE4  Infiltrator demo

 简介

        大约一年前,我们决定投入一些时间来改进我们的着色模型,并采用更加基于物理的材质工作流程。这是由于我们想要渲染更加真实的图像,但同时我们也对更加物理化的材质创建方法和材质分层的使用可实现的目标感兴趣。美术认为这将是对工作流程和质量的巨大改进,而且我们在另外一个工作室中也亲眼看到了这些好处,在那里我们过度到使用离线结合的材质层。Epic的一位技术美术尝试在着色器中使用分层,结果令人满意,因此这成为了一个附加的需求。

        为了支持这一方向,材质的分层需要简单有效。完美的时机出现了,迪士尼发布了他们用于Wreck-It Raph的物理着色和材质模型的演示。Brent Bruley证明,一小部分材质参数足以用于离线剧情的渲染。他还表明,一个相当实用的着色模型可以贴切地适用于大多数采样材质。他们的工作称为我们的灵感和基础,就像他们的"原则“一样,我们也决定为我们自己的系统指定了目标:

        实时性能

        • 首先,需要高效率地同时使用多个可见灯光。

        降低复杂度

        • 参数应该尽可能少,大量参数会使决策比较困难,需要不断试验容易引起错误,或者需要针对单个预期效果更改许多属性。

        • 我们需要能够交替使用基于图像的光源和分析光源,因此参数必须在所有光源类型中保持一致。

        直观的交互

        • 我们更希望简单易懂的值,而不是像折射率这样的物理值。

       感知线性

        • 我们希望通过蒙版来支持分层,但是我们只能为每个像素着色一次,这意味着参数混合的着色必须尽可能与着色结果的混合匹配。

       易于掌握

        • 我们希望避免还需要理解电介质和导体等概念,并尽量减少创建基本基于物理材质所需的工作量。

        健壮的

        • 创建出物理上错误的材质应该是很困难的。

        • 所有参数组合应该尽可能稳定可靠。

        表达能力强的

       • 延迟着色限制了我们可以拥有的着色模型数量,因此我们的基础着色模型需要具有足够的描述能力,以覆盖现实世界中99%的材质。

        • 所有可分离的材质需要共享同一组参数,以便在它们之间进行混合。

        灵活

        • 其它项目和使用者可能无法实现照片写实的相同目标,因此需要足够灵活来实现非真实感渲染。

着色模型

漫反射BRDF

        我们评估了Burley的漫反射模型,但与Lambertian漫反射(方程式1)相比只看到了微小的差异,因此我们无法证明额外成本的合理性。此外,任何更复杂的漫反射模型都难以有效地与基于图像或球面的谐波照明配合使用。因此,我们没有投入太多精力来评估其它选择。

        

         其中是材质的漫反射反照率。

微平面镜面BRDF

        一般的Cook-Torrance microfacet镜面着色模型是:

        

        我们从迪士尼的模型开始,并评估每一项与更有效的替代品的重要性。这一过程比听起来更加困难,因为每个项的公布公式不一定使用相同的输入参数,这对于正确比较至关重要。

镜面D

        对于法线分布函数(NDF),我们发现迪士尼对GGX/Trowbridge-Reitz的选择非常好,使用Blinn-Phong的额外消耗小,而较长的”拖尾“产生的独特自然外观吸引了我们的美术。我们还采用了迪士尼的的重新参数化。

        

镜面G

        我们评估了镜面几何衰减项的更多选项。最终,我们选择使用Schlick模型,但使用 k = α/2,以便更好地拟合GGX的Smith模型。通过这种修改,Schlick模型可以精确地匹配Smith的α = 1,并在范围[0 , 1]上是非常接近的近似值(如图2所示)。我们还选择了迪士尼的修改,在平方之前对粗糙度做重映射

        重要的是要注意,这种调整仅用于分析光源,如果应用于基于图像的照明,掠射角度的结果将太暗。

        

图2 Schlick使用k=α/2与Smith非常接近

镜面F

        对于菲涅尔,我们做出了使用Schlick近似的典型选择,但稍作修改:我们使用球面高斯近似来代替共v了。计算效率稍高,差异较难察觉,公式是:

        

        其中F0是垂直入射时的镜面反射率。

基于图像的照明

        要将此着色模型与基于图像的光照一起使用,需要求解辐照度积分,这通常使用重要性采样来完成,以下等式描述了这种数值积分:

         

         以下HLSL代码演示了我们的着色模型是如何工作的:

float3 ImportanceSampleGGX( float2 Xi, float Roughness, float3 N )
{
    float a = Roughness * Roughness;

    float Phi = 2 * PI * Xi.x;
    float CosTheta = sqrt( (1 - Xiy) / ( 1 + (a*a - 1) * Xi.y ) );
    float SinTheta = sqrt( 1 - CosTheta * CosTheta );

    float3 H;
    H.x = SinTheta * cos( Phi );
    H.y = SinTheta * sin( Phi );
    H.z = CosTheta;

    float3 UpVector = abs(N.z) < 0.999 ? float3(0,0,1) : float3(1,0,0);
    float3 TangentX = normalize( cross( UpVector, N) );
    float3 TangentY = cross( N, TangentX );
    
    // Tangent to world space
    return TangentX * H.x + TangentY * H.y + N * H.z;
}

float3 SpecularIBL( float3 SpecularColor, float Roughness, float3 N, float3 V)
{
    float3 SpecularLighting = 0;

    const uint NumSamples = 1024;
    for( uint i = 0;i < NumSamples; i++)
    {
        float2 Xi = Hammersley(i, NumSamples);
        float3 H = ImportanceSampleGGX( Xi, Roughness, N );
        float3 L = 2 * dot(V,H) * H - V;

        float NoV = saturate( dot( N, V) );
        float NoL = saturate( dot( N, l) );
        float NoH = saturate( dot( N, H) );
        float VoH = saturate( dot( V, H) );

        if( NoL > 0 )
        {
            float3 SampleColor = EnvMap.SampleLevel(EnvMapSampler, L, 0).rgb;

            float G = G_Smith( Roughness, NoV, NoL);
            float Fc = pow(1 - VoH, 5);
            float F = (1 - Fc) * SpecularColor + Fc;

            // Incident light = SampleColor * NoL
            // Microfacet specular = D*G*F / (4*NoL*NoV)
            // pdf = D * nOh / (4 * VoH)
            SpecularLighting += SampleColor * F * G * VoH / ( NoH * NoV);
        }
    }
    return SpecularLighting / NumSamples;
}

        即使进行重要性采样,仍需要采集许多样本。使用mipmap贴图可以明显减少样本数量。但是为了获得足够的质量,仍需要大于16的采样数。因为我们在每个像素的许多环境贴图之间进行混合以进行局部反射,所以我们实际上只能为每个提供一个采样。

分裂和近似

        为了实现这一点,我们通过将其分成两个和来近似上述总和(等式7),然后可以预先计算每个单独的总和。这种近似对于常数Li(l)是精确的,并且对于常见环境是相当准确的。

        

预过滤环境映射

       我们预先计算不同粗糙度值的第一个总和,并将结果存储在立方体贴图的mip-map级别中。这是游戏行业中许多人使用的典型方法。一处微小的差异是我们使用重要性采样将环境贴图与我们的着色模型的GGX分布通过卷积处理在一起。由于它是微平面模型,分布的形状随着对表面的视角而改变。因此我们先假设该角度为0,即N = V = R。这种各向同性假设是近似的第二个来源,遗憾的是,我们不会在掠射角处获得长时间的反射。与分裂和近似相比,这实际上是我们的IBL解决方案的更大误差源。如下面的代码所示,我们发现cos θlk的加权可以获得更好的结果。

float3 PrefilterEnvMap(float Roughness, float3 R)
{
    float3 N = R;
    float3 V = R;
    
    float3 PrefilteredColor = 0;

    const uint NumSamples = 1024;
    for(uint i = 0; i < NumSamples; i++)
    {
        float2 Xi = Hammersley(i , NumSamples);
        float3 H = ImportanceSampleGGX(Xi, Roughness, N);
        float3 L = 2 * dot(V, H) * H - V;
  
        float NoL = saturate(dot(N, L));
        if(NoL > 0 )
        {
             PrefilteredColor += EnvMap.SampleLevel(EnvMapSampler, L, 0).rgb * NoL;
             TotalWeight += NoL;
        }
    }
    return PrefilteredColor / TotalWeight;
}

环境BRDF

        第二个总和包含了其他内容。这和使用纯白环境积分镜面反射BRDF一样,例如,。通过替换Schlick的菲涅尔近似值,我们发现F0可以从积分中分解出来:

        

        这样就留下了两个输入(粗糙度和)以及两个输出(F0的缩放和偏移),所有值都恰好落在区间[0, 1]。

        我们重新计算了这一函数的结果,并将其存储在一张2D查找纹理中(LUT).

        在完成了这一操作后,我们发现了几乎所有现有研究,都和我们提供了几乎相同的解决方案。在Gotanda使用3D LUT的同时,Drobot将其优化为2D LUT,与我们所做的方法大致相同。此外,作为本课程的一部分,Lazarov在此基础上做了扩展,也就是为相似的积分提供几个解析近似值。

float2 IntegrateBRDF(float roughness, float NoV)
{
    float3 V;
    V.x = sqrt(1.0f - NoV * NoV); // sin
    V.y = 0;
    V.z = NoV; // cos

    float A = 0;
    float B = 0;

    const uint NumSamples = 1024;
    for(uint i = 0; i < NumSamples; i++)
    {
        float2 Xi = Hammersley(i, NumSamples);
        float3 H = ImportanceSampleGGX(Xi, Roughness, N);
        float3 L = 2 * dot(V, H) * H - V;

        float NoL = saturate(L.z);
        float NoH = saturate(H.z);
        float VoH = saturate(dot(V, H));

        if (NoL > 0)
        {
            float G = G_Smith(Roughness, NoV, NoL);
            float G_Vis = G * VoH / (NoH * NoV):
            float Fc = pow(1 - VoH, 5);
            A += (1 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }
    return float2(A, B) / NumSamples;
}

       最后,为了近似重要性采样参数,我们将两个预先计算的总和相乘。

float3 ApproximateSpecularIBL(float3 SpecularColor, float Roughness, float3 N, float3 V)
{
    float NoV = saturate(dot(N, V));
    float3 R = 2 * dot(V, N) * N - V;

    float3 PrefilteredColor = PrefilterEnvMap(Roughness, R);
    float2 EnvBRDF = IntergrateBRDF(Roughness, NoV);

    return PrefilteredColor * (SpecularColor * EnvBRDF.x + EnvBRDF.y);
}

        顶部是参考效果,中间为分裂和近似,底部为假设n=v时的近似。镜像对称假设引入的误差最大,但组合近似依然和参考值非常相近。

        和图4类似的对比,对象是电介质。

材质模型

        我们的材质模型是迪士尼的简化版,更关注实时渲染的效率。限制参数的数量对于优化GBuffer空间,减少纹理存储和访问,以及减少在像素着色器中混合材质层的消耗。

        下面是我们的基础材质模型:

        BaseColor(基础颜色) : 单一颜色。比较容易理解的概念。

        Metallic : 无需了解电介质和导体的反射率,因此出错的概率较小

        Roughness: 含义很清楚,光泽度则总是需要解释

        Cavity:用于小规模阴影(注:目前已经没了)

        BaseColor, Metallic和Roughness与迪士尼的模型相同。但Cavity参数是不存在的,因此需要解释一下。Cavity用于处理运行时阴影算法无法处理的几何体的特殊阴影,这通常是因为几何图形信息仅存储于法线贴图。例如地板之间的缝隙或衣服的接缝。

       最明显的一个遗漏参数是“镜面反射”,实际上,直到Infiltrator演示完成之前,我们一直在使用此功能,但我们并不喜欢这一功能。首先,我们觉得镜面反射是一个糟糕的参数名称,这引起了不少混淆,习惯于控制镜面反射的美术可能难以适应粗糙度的控制。此外,美术和图形程序通常都会忘记它的范围,并假定默认值为1,但是实际上它的默认值是Burley的0.5倍(对应于4%的反射率)。有效使用镜面反射的情况主要用于小规模阴影。我们发现可变折射率(IOR)对非金属而言并不重要,因此我们使用更容易理解的Cavity参数替换了Specular。

 

        以下是迪士尼模型的参数,我们选择不将其作为基础参数,而是将其看作特殊情况:

        次表面:与阴影贴图不同的方式采样

        各向异性:需要多个IBL采样

        透明涂层(Clear Coat) : 需要两次IBL采样。

        光泽度:在Burley的笔记中未定义

        除了我们在Elemental演示中用于制作冰的次表面散射外,我们还没有在实际情况中使用这些特殊情况的模型。此外,我们还有专门针对皮肤的着色模型。之后我们还在考虑采用延迟/前向混合着色算法,来支持更好的着色模型。当前,使用延迟管线,我们可以通过动态分支来处理GBuffer中存储的着色模型ID对应的不同着色模型。

实践

        我已经多次遇到了这种情况。我告诉美术开始转向使用粗糙度变化。“使用粗糙度就像你们之前使用高光颜色一样”。不久后我听到了令人惊讶的好消息“它起作用了”。但是随之而来的一个有趣的评论是“粗糙度感觉像是反过来了”。实际上,美术希望看到随着纹素更亮纹理中的高光也会更亮,如果图像存储的是粗糙度,更亮的值意味着越粗糙,这将导致亮度降低。

        我无数次收到的问题是“金属度是一个二值吗?”我最初会向这些人解释混合或分层材质的微观模型。后来我意识到只需要回答“是的“。这是因为,美术刚开始不愿意将参数设为绝对值。我发现金属值通常被设为0.8。接下来我们将讨论材质分层,描述99%金属度既不是0也不是1的情况。

        在过渡期间,我们遇到了一些问题,这些材质的参数将无法继续复制使用。其中最重要的一个来自Fornite,这是Epic目前正在制作的游戏。

        Fortnite为非真实感渲染方向,并且故意使用了互补色来实现漫反射和镜面反射,这并非物理正确的,因此无法在新的材质模型中演示。经过长时间的讨论,我们决定通过引擎开关的方式继续支持旧的漫反射颜色镜面反射颜色,来确保Fornite的品质,因为它尚处于开发阶段。但是,我们不认为新的模型不适用于迪士尼在Wreck-It Ralph中应用的非真实感渲染,因此我们打算将其应用于未来的所有项目。

分层材质

        以前的方法是为每个特定的模型通过纹理分别指定材质的参数,混合来自公共链接的材质层相比起原有的方法有很多好处

        ● 重用跨多个资产的工作

        ● 降低单个资产的复杂度

        ● 统一并几种定义游戏表现材质,从而可以更轻松地进行艺术和技术的指导。

        为了更好适应我们的新工作流,我们需要重新考虑一下我们的工具。在ue3时代,Unreal使用基于材质编辑器的结点图表。这一节点图表指明了输入(纹理,常量),操作以及输出,并被编译为shader代码。

        尽管材质分层是这项工作的主要目标,但令人惊讶的时,我们并不需要开发工具来支持多层材质的制作和混合。ue4材质编辑器中的节点图表模块已经可以分组到各个函数,并可以在多个材质中被使用。

        这一功能非常适用于材质分层的实现。可以将材质层保留在基于节点的编辑器中,而不是将其作为顶层的固定的函数系统,从而允许以可编程的方式映射和组合材质层。

        为了简化工作流程,我们添加了一个新的数据类型,即材质属性,该数据类型包含所有的材质输出数据。与我们其它类型一些,这种新类型可以通过单一引脚通过连线输入或输出材质函数。要么通过连线传递,要么直接输出。通过这些更改,可以像以前使用纹理一样将材质层作为输入进行拖动、组合修改和输出。实际上,大多数材质图表会更加简单,因为采样图层作为特定材质后,需要指定的也就是如何映射和混合图层。这比过去存在的参数特定操作要简单得多。

        由于具有较少的线性材质参数,因此实际上可以在着色器中完全混合图层。我们认为,与单纯的离线复合系统相比,这可以显著的提高质量。由于能够以不同的频率映射数据,因此纹理数据的表面分辨率可能分厂高:每个顶点或低频纹理数据可能是唯一的,每个网格都指定了图层混合蒙版,法线贴图或cavity贴图,其材质层是平铺在网格表面上的,更高级的情况可能会使用更多的频率。

        尽管由于着色器成本的原因,我们实际上能够使用的层数会受到限制,但我们的美术尚未发现这个问题。

        另外一个值得关注的问题是,在某些情况下,美术通过将网格划分为多个部分来解决着色器内的分层限制,从而带来更多的drawcall。虽然我们预计由于CPU端代码的优化会带来更优的drawcall次数,但这看起来似乎是将来出现问题的根源。我们尚未尝试过使用动态分支来降低图层覆盖率达到百分百区域的着色器成本。

        到目前为止,我们在材质层的经验一直都在往好的方向发展。我们已经看到了生产效率和质量的大幅提高。我们希望通过提供更方便的查询方式让美术能够更熟悉材质层的调用库。除了当前的运行时系统,我们还打算研究一种离线/烘焙的系统,以支持更多的层并提供更好的伸缩性。

 

以上是关于[图形学] Real Shading in Unreal Engine 4的主要内容,如果未能解决你的问题,请参考以下文章

《Real-Time Rendering》第四版学习笔记——Chapter 5 Shading Basics

《Real-Time Rendering》第四版学习笔记——Chapter 5 Shading Basics

《Real-Time Rendering》第四版学习笔记——Chapter 9 Physically Based Shading

《Real-Time Rendering》第四版学习笔记——Chapter 9 Physically Based Shading

《Real-Time Rendering》第四版学习笔记——Chapter 5 Shading Basics

《Real-Time Rendering》第四版学习笔记——Chapter 5 Shading Basics