可可西

3D渲染相关基本概念

渲染管线

图形渲染管线(Graphics Pipeline:将三维模型渲染到二维屏幕上的过程。为了满足实时性,管线在GPU硬件上进行实现,其与CPU流水线一样,各个步骤都会以并行的形式运行。

固定管线(Fixed-Function Pipeline):通常是指在较旧的GPU上实现的渲染流水线,通过DX、OpenGL等图形接口函数,开发者来对渲染流水线进行配置,控制权十分有限。

可编程管线(Programmable Pipeline):随着人们对画面品质和GPU硬件能力的提升,在原有固定管线流程中插入了Vertex Shader、Geometry Shader(非必需)、Fragment(Pixel) Shader等可编程的阶段,让开发者对管线拥有更大控制权

例如:Vertex Shader修改顶点属性(如顶点空间变换、逐顶点关照、uv变换)以及通过自定义属性向管线传入一些数据,Geometry Shader可增删和修改图元,Fragment(Pixel) Shader来进行逐像素的渲染

 

管线资源

材质(Material):用于描述光与物体的交互过程,即:如何反射(包括漫反射和镜面反射),折射,透射等。用于控制物体的视觉外观。是一些程序(即shader)、贴图以及其他属性的集合体。

          在执行光照计算时,需要用到一些材质属性才能得到表面的最终颜色。

          常见的几种属性有Diffuse(漫反射)、Emissive(自发光)、Specular(高光)、Normal(法线)等。

着色器(Shader):是执行在GPU上可编程图形管线的代码片段,用于告诉图形硬件如何绘制物体。包括:Vertex Shader、Fragment(Pixel) Shader、Geometry Shader。

Shader在早期是用汇编来编写的,后面出现更高级的着色语言,如DirectX的HLSL(High Level Shading Language)、OpenGL的GLSL(OpenGL Shading Language)以及nVidia的Cg(C for Graphic)。

 

GLSL具有跨平台性,其在被OpenGL使用前不需要进行额外编译,而是由显卡驱动直接编译成GPU使用的机器指令。注:OpenGL4.1以上,可以通过glGetProgramBinary回读编译好的机器码,在相同的gpu驱动下避免二次编译。

HLSL仅能在windows平台上使用,需预先编译成与硬件无关的DX中间字节码(DXBC/DXIL,注:D3D12使用DirectX Shader Compiler,工具为dxc.exe,将HLSL编译成DXIL;之前的D3D版本使用d3dcompiler,工具为fxc.exe,将HLSL编译成DXBC)才能被D3D使用。

注:相比d3dcompiler,dxc虽然也需要将HLSL二次编译成GPU的机器码,但是我们可以回读这些机器码,然后缓存取来,这样就可以在相同的gpu驱动下避免二次编译。

CG语法上与HLSL高度相似,具有真正意义上的跨平台:在不同的平台上实现了shader编译器,并通过CG OpenGL Runtime和CG D3D Runtime来将CG转换成GLSL和DX中间字节码。

纹理(Texture):可以理解为运行时的贴图,可以通过UV坐标映射到模型的表面。另外,其拥有一些渲染相关的属性,如:纹理地址模式(ADDRESSU、ADDRESSV),纹理过滤方法(MAGFILTER、MINFILTER、MIPFILTER)等

 

图形API概念

DrawCall:为CPU向GPU发起的一个命令(如:OpenGL中的glDrawElements函数、D3D9中的DrawIndexPrimitive函数、D3D11中的Draw、DrawIndexed函数)。

                  这个命令仅仅会指向一个需要被渲染的图元(primitives)列表(IBO,Index Buffer Object)。发起DrawCall时,GPU就会根据渲染状态(RenderState)和输入的顶点数据(VBO,Vertex Buffer Object)来计算,最终输出成屏幕上显示的像素。

渲染状态(RenderState):这些状态定义了场景中Mesh是怎样被渲染的。如:使用哪个vs、哪个fs、光源属性、材质、纹理、是否开启混合等

颜色缓冲区(Color Buffer):即帧缓冲区(Frame Buffer,Back Buffer),用于存放渲染出来的图像。D3D存放在一个RTV(RenderTargetView)中。

深度缓冲区(Depth Buffer):用于存放深度的图像。D3D存放在一个DSV(DepthStencilView)中。

模板缓冲区(Stencil Buffer):用于获得某种特定效果的离屏缓存。分辨率与颜色缓冲区及深度缓冲区一致,因此模板缓冲区中的像素与颜色缓冲区及深度缓冲区是一一对应的。

                                                  其功能与模版类似,允许动态地、有针对性地决定是否将某个像素写入后台缓存中

                                                  D3D中,与深度缓冲区一起存放在一个DSV(DepthStencilView)中。

表面(Surface):D3D在显存中用于存储2D图像数据的一个像素矩阵。D3D9中对应的COM接口为IDirect3DSurface9。

Render Target(RT,渲染目标):对应显卡中一个内存块, D3D中概念(OpenGL中叫做FBO,Framebuffer object),常用于是离屏渲染。

渲染管线默认使用后备缓冲区(BackBuffer)RT来存放渲染结果,可通过调用CreateRenderTarget或RTT来创建多个额外的RT来进行离屏渲染,最后将它们组装到后备缓冲区(BackBuffer)中以产生最终的渲染画面。

注1:调用Device->CreateRenderTarget创建RT成功后,会返回IDirect3DSurface9* pRTSurface;然后调用Device->SetRenderTarget(0, pRTSurface)绑定pRTSurface到指定的RT索引。

         在执行SetRenderTarget前可调用Device->GetRenderTarget(0, pOriginRTSurface),以便在完成RT绘制后还原回pOriginRTSurface所指向RT的Surface

         对于不支持MRT的显卡,只会有一个索引为0的RT;对于支持MRT(N个)的显卡,索引可以为0,1, ...N-1,可同时绑定N个Surface到N个RT的索引上

注2:成功绑定RT后:对于不支持MRT的显卡,在Pixel Shader中通过标识COLOR0来写入内容到索引为0的RT中;对于支持MRT(N个)的显卡,在Pixel Shader中通过标识COLOR0, COLOR1, ...COLOR(N-1)来写入内容到对应的RT中

注3:可以调用Device->StretchRect来将RT的Surface拷贝到后备缓冲区(BackBuffer)或者另外一个Surface中

注4:可以调用Device->GetBackBuffer(0,0,D3DBACKBUFFER_TYPE_MONO,&pRTBackBuffer))来得到后备缓冲区(BackBuffer)的Surface

进一步可参考:Render to Surface

RTT(Render To Texture,渲染到纹理):用法与上面的RT一致,只是先调用Device->CreateTexture来创建出IDirect3DTexture9* pTexture,然后再通过pTexture->GetSurfaceLevel(0, IDirect3DSurface9**)来创建RT返回IDirect3DSurface9* pRTSurface

注:与上面RT相比,RTT不支持MultiSample   进一步可参考:渲染到纹理(Render To Texture, RTT)详解

MRT(Multiple Render Targets,多渲染目标):在一个pass(如:几何Pass,Geometry Pass)中将渲染信息保存到多个render target,并可在后续的管线流程中被其他shader使用或作为3D模型的纹理使用。

MRT需要显卡硬件和图形API(从OpenGL2.0和D3D9起)支持(对于不支持MRT的硬件可以使用多次渲染解决),提供了单个pass同时操作多个render target的能力,

MRT技术提高了渲染过程存储中间数据(如:Normal、Diffuse、Depth、Specular和Shininess等)的容量和渲染效率,是一种典型空间换时间的例子。注:Shininess值决定Specular光圈的大小

然而使用多个render target来渲染也并不是百利无一害,多个render target的读写IO开销也会不小,因此要紧凑地使用render target各bit来存储信息,能用一张就不要用两张,要尽量省以提升渲染效率。

(1) D3D9使用DirectX Caps Viewer查看显卡支持MRT个数

(2)D3D9可使用下列方法查询显卡支持MRT个数

D3DCAPS9 DeviceCaps;
UINT AdapterIndex = D3DADAPTER_DEFAULT;
D3DDEVTYPE DeviceType = D3DDEVTYPE_HAL;
if(SUCCEEDED(Direct3D->GetDeviceCaps(AdapterIndex,DeviceType,&DeviceCaps)))
{
    UINT32 MRTCount = DeviceCaps.NumSimultaneousRTs; // 支持MRT的个数
}

(3)D3D10最大支持MRT个数为D3D10_SIMULTANEOUS_RENDER_TARGET_COUNT // 8

         D3D11最大支持MRT个数为D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT // 8

         实际支持多少个MRT由硬件显卡决定。mobile平台上,MRT最多只有4个,每个的bit数最多为32。

MRT的限制:

① 设定的RT需要具有相同的宽高

② 设定的RT一般需要有相同的位宽,比如同是16bits或32bits

 

G-Buffer(Geometry Buffer,几何缓冲区):延迟渲染管线中,在渲染场景物件时,用来存储几何和材质信息的RT图像(实际实现中常将各信息紧凑地挤进1个RT或MRT技术多个RT中),使得能在屏幕空间进行光照计算

 

Render Pass:现代游戏引擎会将渲染过程划分成多个Render Pass,一个模型会在多个Pass进行绘制处理。

例如UE的Mobile前向渲染,会先在PrePass中画带Mask透明材质模型的深度(用于后续Pass的EarlyZ剔除来减少OverDraw),然后再在BasePass中对模型进行着色,最后在Translucency的Pass中处理半透明的物体。

又比如阴影,需要先渲染深度图,再渲染场景,需要2个Pass。

Pass之间是相互依赖的,后面的Pass会用到前面Pass的数据(深度、几何信息),最后的Pass出来的数据才会写进FrameBuffer,所以Pass之间的关系可以形象的比喻为一道道工序。

 

渲染路径(Rendering Path)

前向渲染(Forward Rendering):逐光源逐物体进行光照计算

For each light:
    For each object affected by the light:
        framebuffer += object * light

对于多光源,使用Forward Rendering的效率会极其低下。 因为如果在vs中计算光照,其复杂度将是 O(num_geometry_vertexes∗num_lights)

而如果在fs中计算光照,其复杂度为 O(num_geometry_fragments∗num_lights) 。可见光源数目和复杂度是成线性增长的 。

 

延迟渲染Deferred Shading:最早由Michael Deering在1988年的论文The triangle processor and normal vector shader: a VLSI system for high performance graphics提出,是一种基于屏幕空间着色的技术。

             它的核心思想是将场景的物体绘制分离成两个Pass:几何Pass光照Pass,目的是将计算量大的光照Pass延后,和物体数量和光照数量解耦,以提升着色效率。

             计算光照的复杂度为 O(num_geometry_fragments + num_lights) 。在目前的主流渲染器和商业引擎中,有着广泛且充分的支持。

 

光照(lighting)

球谐光照(SH,Spherical harmonic lighting):用来计算低频环境光(即环境光diffuse部分)。用于Irradiance Volume和Precompute Radiance Transfer(PRT)。  https://huailiang.github.io/blog/2019/harmonics/

球谐光照实际上是一种对光照的简化,对于空间上的一点,受到的光照在各个方向上是不同的,也即各向异性,所以空间上一点如果要完全还原光照情况,那就需要记录周围球面上所有方向的光照。

注意这里考虑的周围环境往往是复杂的情况,而不是几个简单的光源,如果是那样的话,直接用光源的光照模型求和就可以了。
如果环境光照可以用简单函数表示,那自然直接求点周围球面上的积分就可以了。
但是通常光照不会那么简单,并且用函数表示光照也不方便,所以经常用的方法是使用环境光贴图,比如cubemap。

考虑一个简单场景中有个点,它周围的各个方向上的环境光照就是上面的cubemap呈现的,假如我想知道这个点各个方向的光照情况,那么就必须在cubemap对应的各个方向进行采样。
对于一个大的场景来说,每个位置点的环境光都有可能不同,每个点都对应一个cubemap,如果把每个点的环境光贴图储存起来,并且每次获取光照都从相应的贴图里面采样,可想而知这样的方法是非常昂贵的。

利用球谐函数就可以很好的解决这个问题,球谐函数的主要作用就是用简单的系数表示复杂的球面函数。
球谐光照实际上就是将周围的环境光采样成几个系数,然后渲染的时候用这几个系数来对光照进行还原,这种过程可以看做是对周围环境光的简化,从而简化计算过程。

基于图像的光照(Image Based Lighting,IBL):将要反射的“环境”渲染为一张图(比如cubemap),然后渲染时通过查询这个贴图来计算来自周围的环境光照。用来计算环境反射光。https://huailiang.github.io/blog/2019/ibl/

PBR光照方程:

IBL计算环境光diffuse部分:

为了方便进行积分运算,一般都将渲染方程改为球面坐标的积分形式,其中:

所以,方程转变为如下形式:

上述公式转换为Riemann Sum(黎曼和)的表述:

Riemann Sum是一种很简单的积分方法,当我们的步进值越小的时候,通过这种方法计算出来的h值就越加的接近真实值。

vec3 irradiance = vec3(0.0);  

vec3 up    = vec3(0.0, 1.0, 0.0);
vec3 right = cross(up, normal);
up         = cross(normal, right);

float sampleDelta = 0.025;
float nrSamples = 0.0; 
for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta)
{
    for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta)
    {
        // spherical to cartesian (in tangent space)
        vec3 tangentSample = vec3(sin(theta) * cos(phi),  sin(theta) * sin(phi), cos(theta));
        // tangent space to world
        vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 

        irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta);
        nrSamples++;
    }
}
irradiance = PI * irradiance * (1.0 / float(nrSamples));

cubemap在球谐面卷积采样, 生成的irradianceMap图(预计算辐射光照贴图),有点类似模糊的效果,大小就32x32, 精度不需要那么高, 中间值使用插值就可以得到。

 

IBL计算环境光specular部分:

第一部分,我们使用预过滤环境贴图来解决,并且我们把粗糙度加入进去。由于粗糙程度的增加,环境贴图需要更多的离散采样向量和更多的模糊反射。

对于每一个粗糙度等级,我们将其连续的模糊结果储存在预过滤贴图的mipmap等级中。如下例所示:我们将5个不同模糊等级的结果存储在5个mipmap等级的贴图中。

第二部分,该部分就是双向反射分布函数(BRDF)的镜面反射积分。

如果我们假设每个方向的入射光的颜色是白的(即L(p,x)=1.0),在给出粗糙度和法线n与光向量Wi的夹角的情况下,我们可以预计算双向反射分布函数(BRDF)的返回值。

Epic Games 会根据每个法线n与光向量Wi的组合以及粗糙度来存储一个值到2D的查找纹理当中(LUT),这个贴图也叫做BRDF积分贴图。

这个2D的查找纹理输出一个scale(对应于图片的红色分量)和一个偏移值(绿色分量)为菲涅尔方程提供参数。

右边部分要求我们在给出N与W0的夹角、表面粗糙度和菲涅尔F0后对BRDF方程进行卷积。这很像当Li为1.0时对镜面BRDF进行积分。在3个变量的情况下对BRDF进行卷积十分麻烦,但是我们可以把F0移出镜面BRDF方程:

F代表的是菲涅尔方程。将菲涅尔移动到BRDF的分母上,可以得到如下方程:

我们用菲涅尔方程的近似值来代替最右边的菲涅尔方程:

让我们用α来代替(1−ωo⋅h)5(1−ωo⋅h)^5,以便于更方便的解决F0:

然后我们将其分成两部分:

接下来,我们可以把F0提到积分外面,并将α变回原来的式子:

注意!!因为本身f(p,ωi,ωo)包含菲涅尔方程, 所以我们可以将分母上的F与其抵消~

使用一种相似的方式更早的对环境贴图进行卷积,我们可以通过BRDF方程的输入进行卷积:N与W0的夹角和粗糙度以及存储在纹理中的卷积结果。我们将卷积结果存储在一张2D查找纹理中(LUT),也被称为BRDF积分贴图,我们稍后会在PBR光照着色器中使用它来得到简介镜面反射结果。

BRDF卷积着色器对一个2D平面进行操作,使用2D纹理的坐标作为BRDF 卷积的直接输入(NdotV和粗糙度)。这部分的卷积代码与预过滤卷积代码十分的相似,不同之处是它的采样向量是根据BRDF的几何函数和菲涅尔方程近似值得到的:

vec2 IntegrateBRDF(float NdotV, float roughness)
{
    vec3 V;
    V.x = sqrt(1.0 - NdotV*NdotV);
    V.y = 0.0;
    V.z = NdotV;

    float A = 0.0;
    float B = 0.0; 

    vec3 N = vec3(0.0, 0.0, 1.0);
    
    const uint SAMPLE_COUNT = 1024u;
    for(uint i = 0u; i < SAMPLE_COUNT; ++i)
    {
        // generates a sample vector that's biased towards the
        // preferred alignment direction (importance sampling).
        vec2 Xi = Hammersley(i, SAMPLE_COUNT);
        vec3 H = ImportanceSampleGGX(Xi, N, roughness);
        vec3 L = normalize(2.0 * dot(V, H) * H - V);

        float NdotL = max(L.z, 0.0);
        float NdotH = max(H.z, 0.0);
        float VdotH = max(dot(V, H), 0.0);

        if(NdotL > 0.0)
        {
            float G = GeometrySmithIBL(N, V, L, roughness);
            float G_Vis = (G * VdotH) / (NdotH * NdotV);
            float Fc = pow(1.0 - VdotH, 5.0);

            A += (1.0 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }
    A /= float(SAMPLE_COUNT);
    B /= float(SAMPLE_COUNT);
    return vec2(A, B);
}

void main() 
{
    vec2 integratedBRDF = IntegrateBRDF(TexCoords.x, TexCoords.y);
    FragColor = integratedBRDF;
}

我们生成的这个查找纹理的水平分量代表BRDF的输入n*Wi,竖直分量则是输入的粗糙度。当我们拥有BRDF积分贴图和预过滤环境贴图后,我们可以将两者合并,并得到镜面积分结果:

float lod  = getMipLevelFromRoughness(roughness);
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);
vec2 envBRDF  = texture2D(BRDFIntegrationMap, vec2(roughness, ndotv)).xy;
vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

 

局部光照:只考虑光源对物体的影响(直接光照),不考虑光线被不同的物体表面反射而产生的间接光照。

全局光照(GI,global illumination):模拟光线是如何在场景中传播的,不仅会考虑那些直接光照(direct illumination)的结果,还会计算光线被不同的物体表面反射而产生的间接光照(indirect illumination)。

                  在使用基于物理的着色技术时,当渲染表面上一点时,我们需要计算该点的半球范围内所有会反射到观察方向的入射光线的光照结果,这些入射光线中就包含了直接光照和间接光照。

 

光线追踪(Ray Tracing):与传统的光栅化渲染方式不同,光线追踪(Ray tracing)是三维计算机图形学中的特殊渲染算法,追踪从摄像机发出的光线而不是光源发出的光线,通过这样一项技术生成编排好的场景的数学模型显现出来。

                                             这种方法有更好的光学效果,例如对于反射与折射有更准确的模拟效果。光线追踪(Ray tracing)有很多算法实现,常见的有路径追踪(Path Tracing)光子映射(Photon Mapping)等。

 

路径追踪(Path Tracing):从屏幕像素(即:摄像机)发射射线,射线未击中任何物体:返回Black或天光。射线击中物体:计算反弹光线并继续执行路径追踪。

                  击中任何光源,则计算该路径上的光照。超出反弹次数未击中任何光源,则返回Black或物体自发光

                  然后用蒙特卡洛方法,计算光线的贡献,作为像素的颜色值。简单来说,路径追踪(Path Tracing)  = 光线追踪(Ray Tracing) + 蒙特卡洛方法。

                  优点:实现容易,几乎可以模拟任何全局光照效果(AO、焦散、间接光等)。缺点:收敛速度慢(噪点多)

   

   

光子映射(Photon Mapping)

   第一个Pass:从光源发射光子,光子与场景物体碰撞,根据Diffuse反射系数d和Specular反射系数s决定光子行为,对于吸收的光子,存入PhotonMap(kd-Tree)

   第二个Pass:从屏幕像素发射射线,射线击中场景,从击中位置在kd-Tree中搜索指定半径r内的光子,然后计算光照

 

LightMap:离线烘焙LightMap来实现静态场景、静态光源的GI。

LightMass的Lightmap是光子映射(Photon Mapping)计算的,新出的GPU LightMass的Lightmap是纯路径追踪(Path Tracing)计算的。

  

 

Irradiance Volume:用于动态物体。动态物体通过采样附近的Probe的辐照度(irradiance)进行球谐光照计算。

   预计算 -- 在空间中生成Probe,为每个Probe计算该位置的Irradiance,并使用前2到3阶的SH(球谐)进行编码

   运行时 -- 采样影响某位置的Probe,计算该位置的Irradiance

(1)unity中为Light Probe:手动摆放Probe,四面体插值来计算权重

    

(2)ue4提供2种方法:即Indirect Lighting Cache(ILC)Volumetric LightMap(VLM)

         

         ILC:自动生成采样点,根据到每个ILC点的距离和ILC点的半径计算权重。注1:对应上图的Sparse Volume Lighting Samples选项   注2:从Show按钮菜单 -- Visualize -- Volume Lighting Samples来显示这些正方形

         

         VLM:自动生成采样点,生成3DTexture。注1:对应上图的Volumetric Lightmap选项   注2:从Show按钮菜单 -- Visualize -- Volumetric Lightmap来显示这些球

         

 

Precomputed Radiance Transfer(PRT):在LightMap和Irradiance Volume基础上,存了一份transfer texture,用于实现静态物体和动态物体的动态光照GI。

  

 

环境光遮蔽(Ambient Occlusion,简称AO):是全局光照明的一种近似替代品,可以产生重要的视觉明暗效果,通过描绘物体之间由于遮挡而导致的环境光变暗, 能够更好地捕捉到场景中的细节。

              可以解决漏光,阴影漂浮等问题,改善场景中角落、锯齿、裂缝等细小物体阴影不清晰等问题,增强场景的深度和立体感。

              阴影与AO的区别:阴影与光源是强相关的,即有光才会有影。而AO是对一种全局环境光由于遮挡而导致变暗的描述。

              场景中的静态物体的AO信息可提前烘焙到lightmap,运行时直接使用。一些不参与烘焙的物体,可通过AO贴图来获取AO信息。

              对于动态物体、只有自身AO信息的物体,是没有物体间的遮蔽信息的,这个时候就需要实时AO了,常见的有SSAO、GTAO。

              

 

              SSAO(Screen Space Ambient Occlusion),屏幕空间AO,用于延迟管线。

              GTAO(Ground Truth Ambient Occlusion),UE4.26在移动端集成了该AO,具体实现在:FMobileSceneRenderer::RenderAmbientOcclusion函数中。

 

Shading着色

逐顶点着色:在VS中进行着色计算

Flat Shading平直着色法):认为在同一多边形上任意点的法线都相同,因此多边形上所有像素为同一颜色值。该方法简单,常用于高速渲染。

Gouraud ShadingGouraud着色法):用多边形顶点颜色进行双线性插值得到内部各像素的颜色,会得到较为平滑的颜色渐变。

渲染一些与相机位置相关的光照效果(比如高光)时,得到的效果会有问题。如果在多边形的中心有高光,而且这个高光没有扩散到该多边形的任何顶点,高光将不会被渲染出来。

而如果正好是多边形的顶点上有高光,那么这个点上的高光是正确的,但插值会导致高光以很不自然的形式扩散到相邻的多边形上。

逐像素着色:在PS中进行着色计算

Phone ShadingPhone着色法):在光栅化阶段,用多边形顶点法线插值得到内部各像素的法线,然后进行像素颜色计算。这种方法计算量大,渲染效果最好。

 

Phong光照:经验模型,没有实际的物理意义。

Ambient(环境光)用来模拟全局光照效果,其实就是在物体光照基础上叠加一个较小的光照常量,用来表示场景中其他物体反射的间接光照。

使用Lambert漫反射模型来计算Diffuse(漫反射光),上图的fd。无论观察者从哪个方向观察,漫反射效果是一样的,所以我们认为漫反射和观察位置是无关的。

漫反射的大小取决于表面法线N和光线L的夹角,即dot(N, -L)。光线越水平,夹角越大,漫反射分量越小;当夹角接近90度时,漫反射几乎为0。

镜面反射与观察方向是有关系的,在描述其性质时,需要知道观察者位置信息。

 

Blinn-Phong光照:Blinn-Phong为优化性能之后的Phong,主要对镜面反射做了改进。

 

PBR(Physically Based Rendering,基于物理的渲染;PBS,基于物理的着色):是一套尝试基于真实世界光照物理模型的渲染技术合集,使用了一种更符合物理学规律的方式来模拟光线和介质表面的交互方式,达到更真实的渲染效果。

PBR模型的好处在于无论光照如何变化,比如从白昼到黑夜,从室内到室外,都会得到比较物理真实的效果。虚幻引擎使用的Disney的PBR模型

PBR材质的基础属性:Base Color(漫反射)、Metallic(金属度)、Roughness(粗糙度)、Specular(高光)

其他属性:Normal(法线)、Ambient Occlusion(环境光遮罩)、World Position Offset(置换纹理)

 

BRDF(Bidirectional Reflectance Distribution Function,双向反射分布函数):不透明物体(可细分为金属与非金属)的反射模型

SBRDF(SVBRDF):一个捕获基于空间位置BRDF变化的函数被称为空间变化的BRDF(Spatially Varying BRDF ,SVBRDF)或称空间BRDF,空间双向反射分布函数(Spatial BRDF ,SBRDF)。

BTDF(Bidirectional Transmittance Distribution Function,双向透射分布函数):半透明物体的折射和透射模型

BSDF(Bidirectional Scattering Distribution Function,双向散射分布函数):BSDF = BRDF + BTDF,BSDF可以看做BRDF和BTDF更一般的形式

BSSRDF(Bidirectional Scattering-Surface Reflectance Distribution Function,双向散射表面反射分布函数):与BRDF的不同之处在于,BSSRDF可以指定不同的光线入射位置和出射位置。如:皮肤、玉、蜡、大理石、牛奶等半透明物体,需用次表面散射模型来渲染

 

NPR(Non-photorealistic rendering,非真实感的渲染:其主要目标是使用一些渲染方法使得画面达到和某些特殊的绘画风格相似的效果,例如卡通、水彩风格等。

Toon Rendering(Cel shading,cell shading,卡通渲染:是NPR的一种。与常规渲染不同的是,卡通渲染的光照效果是经过去真实感处理的。常规光源(明暗间有平滑过渡)的取值被逐一像素计算并投射到一小片独立的明暗区域上,以产生卡通式的单调色彩。

卡通着色基本的三个要素:

① 锐利的阴影(Sharp shadows)
② 少有或没有高亮的点(Little or no highlight)
③ 对物体轮廓进行描边(Outline around objects)

实现方法:

对于含有纹理但没有光照的模型来说,可以通过对纹理进行量化来近似具有实心填充颜色的卡通风格。
对于明暗处理,有两种最为常见的方法,一种是用实心颜色填充多边形区域。但这种方式实用价值不大。另一种是使用 2-tone方法来表示光照效果和阴影区域。
也称为硬着色方法(Hard Shading),可以通过将传统光照方程元素重新映射到不同的调色板上来实现。此外,一般用黑色来绘制图形的轮廓,可以达到增强卡通视觉效果的目的。

 

抗锯齿(Anti-Aliasing,AA)

在信号处理以及相关领域中,走样(Aliasing)是对信号进行采样时,得到与原始信号不匹配的瑕疵。具体分为时间走样(比如在电影中看到车轮倒转等)和空间走样(摩尔纹)。

具体到实时渲染领域中,走样有以下三种:

1. 几何体走样(几何物体的边缘有锯齿),几何走样由于对几何边缘采样不足导致。

2. 着色走样,由于对着色器中着色公式(渲染方程)采样不足导致。比较明显的现象就是高光闪烁。

左图显示了由于对使用了高频法线贴图的高频高光BRDF采样不足时产生的着色走样。右图显示了使用4倍超采样产生的效果。

3. 时间走样,主要是对高速运动的物体采样不足导致。比如游戏中播放的动画发生跳变等。

 

SSAA(超级采样抗锯齿  Super-Sampling Anti-Aliasing):也叫做FSAA(全屏抗锯齿  Full Scene Anti-Aliasing)。比较早期的抗锯齿方法,比较消耗资源,但简单直接。

            这种抗锯齿方法先把图像映射到缓存并把它放大,再用超级采样把放大后的图像像素进行采样。一般选取2个或4个邻近像素,把这些采样混合起来后,还原回原来大小的图像,生成最终像素。

            拿4xSSAA举例子,假设最终屏幕输出的分辨率是800x600, 4xSSAA就会先渲染到一个分辨率1600x1200的buffer上,然后再直接把这个放大4倍的buffer下采样致800x600。

            这种做法在数学上是最完美的抗锯齿,对几何走样着色走样都是有效果的。但是劣势也很明显,光栅化和着色的计算负荷都比原来多了4倍,render target的大小也涨了4倍。

            更多信息详见:Anti-alias的前世今生(一):Hardware AA中SSAA部分

 

MSAA(多重采样抗锯齿  MultiSampling Anti-Aliasing):是一种几何抗锯齿技术,是对点、线、面等基本几何图元进行抗锯齿的一种机制。

            在光栅化时,在每个像素中增加多个 Sample,为每个 Sample 计算像素覆盖率并进行深度模板测试, 这样 Sample 都有各自对应的 Color、Depth/Stencil 值

            最后在 Resolving 时针对这些 Sample 进行评估得到像素的最终颜色,从而达到抗锯齿效果。由于是依赖光栅化机制,因此MSAA 不能解决 Surface 的 Aliasing(如:后处理逻辑中产生的Aliasing)。

            MSAA是一种硬件AA(效率高),根据Sample数分为2x MSAA,4x MSAA,8x MSAA,16x MSAA等。一般只支持前向渲染。

            MSAA时,Depth Stencil也必须是MultiSample的,并且与Color的Sample数量相同。对于MRT的所有Color Buffer,也都必须具有相同的Sample数量。

            移动平台的MSAA可以在TileMemory上实现Multisample,不会带来大量的访问主存的开销,也不会大幅增加显存占用。所以移动平台MSAA是比较高效的。

            但是这并不是说MSAA在移动平台就是免费的了。它依然是有一定开销的,会占用更多的Tite Memory空间,而Tile Memory却是有限的。

       

            当 Sample 执行 Shading 时,使用的顶点属性(Position、UV、Color 等)是经过插值的像素中心的值,而不是每个 Sample 所属位置的插值。由于使用的是同一份插值,因此这意味着每个 Sample 都可以一定程度的代表当前像素

            当最终 Resolving 时平均像素中的 Sample Color 做为像素的最终输出。Resolving 流程示意图如下:

            

            从计算角度看,为了提高性能,几乎所有的硬件都只会对同一个 Primitive(三角形) 覆盖的所有 Sample 进行一次Shading,并通过索引共享一个 Shading 的结果。

            从存储角度看,MSAA 的内存开销是非 MSAA(也可以理解为 Single Sample,只是一个像素只有一个在像素的中心的 Sample)的 N 倍,N 就是对应的 Sample 数量。

            因此,MSAA 的计算效率相对 SSAA 这种每个 Sample 都执行 Shading 的方式要高得多,但带来的问题是当画面中的几何图元覆盖率不断变化时(例如 Camera 或物体的运动),

            Sample 的 Shading 计算次数的不固定导致计算性能不稳定,这也是在工程中使用 MSAA 实现抗锯齿时容易被忽略的一点。下图展示了不同情况下的 Sample 的 Shading 的次数:

            (a) 离Camera从远到近是:不透明的浅青色三角形,半透明的粉色三角形         --》4 个 Sample 中有 3 个被覆盖,当绘制半透明的粉色三角形时,会执行 2 次 Shading 计算,存储在对应的 Sample 的 Color。

             

             (b) 离Camera从远到近是:不透明的浅青色三角形,半透明的粉色三角形,不透明的蓝色三角形         --》绘制蓝色三角形时,所有 Sample 被同一 Primitive(三角形) 覆盖,则只 Shading 一次。

             

             MSAA抗锯齿效果对比:  

                       

 

             MSAA会识别Primitive(三角形)的边缘并进行抗锯齿,所以MSAA实际上把很多计算量浪费在了实际上不必要AA的像素上了。更多信息请参考:Anti-alias的前世今生(三):Hybrid AA

             

           

           最后,需要注意的是MSAA对HDR管线的抗锯齿效果非常差。原因是:在HDR管线中,tonmaping之前的颜色值是HDR的,如果对颜色值相差非常大sample做均值resolve,得到的结果其实是不太尽如人意的。

            

            两组对比数据,左边的像素值差别不大的情况,结果还能接受,右边是像素差别比较大的情况,AA基本失效。  更多信息详见:移动端高性能图形开发 - 详解MSAA

            注:在HDR管线中,如果想使用MSAA取得不错抗锯齿效果,可以通过控制打光范围是LDR的, 保证数值范围在0-1之间

 

FXAA(快速近似抗锯齿  Fast Approximate Anti-Aliasing):也是种取边缘的技术。但是和MSAA不同,MSAA提边缘是在图形管线的前段。

          FXAA是种后处理技术,性能开销最小,后处理技术一般在画面完成后,通过像素颜色检测边缘(色彩差异太大时,不是边缘也被认为成边缘,精度有问题)。优缺点:消耗低,速度快;但是是一种粗糙的模糊处理。

          注:NVIDIA在Graphics SDK 11里面提供了一个称为Fast Approximate Anti-Aliasing的方法。该方法很接近于MLAA,但只识别长边,而不识别形状。有了长边之后,就可以根据边和像素的求交来估算每个像素中sub-pixel的覆盖率,并进行AA混合。

                 后来Timothy Lottes还发展出了FXAA II,质量有所下降,速度提高了。更多请参考:Anti-alias的前世今生(二):Post-process-based-AA

SMAA(增强亚像素形态学抗锯齿  Enhanced Subpixel Morphological Anti-Aliasing):与 FXAA类似,性能消耗比FXAA要高(约两倍左右),但相比FXAA也更清晰。

         SMAA(http://iryoku.com/smaa/)和FXAA都是MLAA(形态学抗锯齿)的一个GPU算法,SMAA注重的是把原算法搬到GPU,FXAA注重的是把原算法的思想简化后在GPU上做的尽量快。

         所以两者的基本算法还是差不多的,都是通过一个像素和周围像素的信息,恢复出局部几何,确定如何AA。但SMAA的搜索更为彻底,所以不是遇到边就模糊了事。这里可以看一组对比。

         这是FXAA的结果。线条不连续,模糊。

         

 

        这是SMAA的结果。线条平滑连续,边缘清晰。

        

 

 

TXAA(时间混叠反锯齿):只能在nVIDIA显卡上用。计算量最大,效果最好的AA算法。常用于电影CG。

MFAA(多帧采样抗锯齿 Multi-Frame AA):与MSAA基于像素采样有所不同,MFAA是基于帧采样的,MFAA在相邻的两帧上各执行一次抗锯齿采样,然后通过NVIDIA自行开发的图像合成处理技术来整合采样结果,最后输出完成抗锯齿运算的图像。

TAA(随机采样抗锯齿, 时域抗锯齿,Temporal Anti-Aliasing):帧渲染时抖动相机0~1个像素内,和历史帧加权平均。会导致画面轻微模糊。消耗非常低,比SMAA还要低。

 

性能上FXAA和TAA不会增加着色像素,具有计算量优势,一般比MSAA要快。TAA相比FXAA则需要额外的buffer保存历史帧,有一定内存和带宽占用。

相比FXAA和TAA,MSAA只影响边缘,效果最好。TAA和FXAA都会造成图像上一定的模糊,一般TAA要比FXAA要好一些。

 

知乎上,文刀秋二对 ”请问FXAA、FSAA与MSAA有什么区别?效果和性能上哪个好?”回答非常好,这里抄过来。

========================================================================================================================

首先所有MSAA, SSAA, FXAA, TXAA等都是抗锯齿(Anti-Aliasing)技术。

锯齿的来源是因为场景的定义在三维空间中是连续的,而最终显示的像素则是一个离散的二维数组。所以判断一个点到底没有被某个像素覆盖的时候单纯是一个“有”或者“没有"问题,丢失了连续性的信息,导致锯齿。

最直接的抗锯齿方法就是SSAA(Super Sampling AA)。拿4xSSAA举例子,假设最终屏幕输出的分辨率是800x600, 4xSSAA就会先渲染到一个分辨率1600x1200的buffer上,然后再直接把这个放大4倍的buffer下采样致800x600。这种做法在数学上是最完美的抗锯齿。但是劣势也很明显,光栅化和着色的计算负荷都比原来多了4倍,render target的大小也涨了4倍。

MSAA(Multi-Sampling AA)则很聪明的只是在光栅化阶段,判断一个三角形是否被像素覆盖的时候会计算多个覆盖样本(Coverage sample),但是在pixel shader着色阶段计算像素颜色的时候每个像素还是只计算一次。例如下图是4xMSAA,三角形只覆盖了4个coverage sample中的2个。所以这个三角形需要生成一个fragment在pixel shader里着色,只不过生成的fragment还是在像素中央(位置,法线等信息插值到像素中央)然后只运行一次pixel shader,最后得到的结果在resolve阶段会乘以0.5,因为这个三角形只cover了一半的sample。现代所有GPU都在硬件上实现了这个算法,而且在shading的运算量远大于光栅化的今天,这个方法远比SSAA快很多。顺便提一下之前NV的CSAA,它就是更进一步的把coverage sample和depth,stencil test分开了。

MSAA的一个问题就是和现在大街小巷都是的deferred shading框架并不是那么兼容。因为用deferred shading的时候场景都先被光栅化到GBuffer上去了,不直接做shading。

硬要做的话可以看我之前写的这个SDK Sample(Antialiased Deferred Shading,大概思路就是用各种方法检测一下哪个pixel是被多个fragment cover的才手动做super sampling)。

因为MSAA这个问题现代引擎里都用的是Post Processing AA这一类技术。这一类东西包括FXAA,TXAA等,不依赖于任何硬件,完全用图像处理的方法来搞。有可能会依赖于一些其他的信息例如motion vector buffer或者前一贞的变换矩阵来找到上一贞像素对应的位置,然后再做一些hack去blur或者blend上一贞的颜色等。通常非常hacky,FXAA的发明人原来是我们组的,他自己都不知道这个为什么会work- -”,但是精心调校之后后效果还是很好的,例如下面是UE4的Post Processing AA开关对比图:

最后再扯一下NV最新的那个MFAA(Multi-Frame AA),因为Maxwell架构支持的programmable coverage sample location,所以可以做到贞间用不同的coverage sample位置,当FPS足够高的时候,2xMFAA就可以达到4xMSAA的效果。

对玩家来说,看着舒服就行。当然像使命召唤这种明明是forward rendering还要用SSAA来抗锯齿的,在显卡烂的机子上开还是要慎重的。。

========================================================================================================================

 

参考

Shaders Overview

【《Real-Time Rendering 3rd》 提炼总结】(四) 第五章 · 图形渲染与视觉外观 The Visual Appearance

深入剖析MSAA

LearnOpenGL-CN(抗锯齿)

游戏引擎随笔 0x15:现代图形 API 的 MSAA

从一个小bug看MSAA depth resolve

posted on 2022-01-05 21:22  可可西  阅读(2193)  评论(0编辑  收藏  举报

导航