unreal 引擎剖析_unreal 引擎 - CSDN
精华内容
参与话题
  • 虚幻引擎源码分析(1)

    千次阅读 2016-11-14 08:30:12
    虚幻引擎源码分析(1)

    虚幻引擎源码分析(1)

    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述
    这里写图片描述

    展开全文
  • UnrealEngine3源码分析笔记

    热门讨论 2020-07-30 23:33:35
    基于虚幻3引擎源码,分析引擎主要技术和设计思路,目前主要以Core库为主,包括对象管理、内存管理、脚本编译、引擎启动流程等。
  • 这一套会讲解UE4引擎是如何初始化的,我们会从WinMain开始讲起,带着大家将UE4引擎初始化代码过一遍,清晰的了解UE4是如何加载模块,如何加载UObject,如何进行静态注册我们的类的,如何利用我们的反射数据等,使我们...

    这一套会讲解UE4引擎是如何初始化的,我们会从WinMain开始讲起,带着大家将UE4引擎初始化代码过一遍,清晰的了解UE4是如何加载模块,如何加载UObject,如何进行静态注册我们的类的,如何利用我们的反射数据等,使我们的知识得到贯通,不仅知道我们的反射数据也知道我们的反射数据如何调用,还知道它是如何注册的等,我们还会讲解UE4在不同百分比下UE4正在干什么,这个对我们以后锁定问题有非常重要的作用。

     

    教程地址:编辑器开发进阶

    展开全文
  • 目录 一、概述 1.1 数字人类的概要 1.1.1 数字人类的历史和现状 1.1.2 数字人类的制作流程 1.2 Unreal Engine的数字人类 1.2.1 Unreal Engine数字人的历史 1.2.2 《Meet Mi...

    一、概述

    1.1 数字人类的概要

    数字人类(Digital Human)是利用计算机模拟真实人类的一种综合性的渲染技术。也被称为虚拟人类、超真实人类、照片级人类。

    它是一种技术和艺术相结合的综合性模拟渲染,涵盖计算机图形渲染、模型扫描、3D建模、肢体驱动、AI算法等领域。

    1617944-20190627164108091-209066990.jpg

    数字人类概念图

    1.1.1 数字人类的历史和现状

    随着计算机渲染技术的发展,数字人类在电影领域早有应用。在上世纪80年代的《星球大战》系列、《异形》系列等电影,到后来的《终结者》系列、《黑客帝国》系列、《指环王》系列等,再到近期的漫威、DC动画电影,都存在着虚拟角色的身影,他们或是天赋异禀的人类,或是奇形怪状的怪物。

    1617944-20190627164133290-725337664.png

    《星球大战I》中的虚拟角色:尤达大师(Master Yoda)

    1617944-20190627164147926-139776452.jpg

    《黑客帝国》的主角很多镜头是采用计算机渲染而成的虚拟数字人

    1617944-20190424235829631-71582043.jpg

    电影《战斗天使》的画面。主角阿丽塔也是虚拟角色。

    由于近些年计算机硬件性能和渲染技术的提升,除了在离线渲染领域的电影和动漫的广泛应用之外,实时领域的应用也得到长足的进步。例如,次世代游戏、3A大作、VR游戏以及泛娱乐领域的直播领域。

    1617944-20190627164206720-2019036777.jpg

    《孤岛惊魂5》中的虚拟游戏角色

    1617944-20190627164243357-633096065.png

    R&D和暴雪在GDC2013展示的次世代虚拟角色

    1617944-20190627164302929-993031766.jpg

    Unreal Engine在GDC2018展示的虚拟角色Siren,可由演员实时驱动动作、表情、肢体等信息。

    1.1.2 数字人类的制作流程

    数字人类的步骤多,工序繁琐。但总结起来,通常有以下几个步骤:

    • 模型扫描。通常借助光学扫描仪或由单反相机组成的360度的摄影包围盒,对扫描对象进行全方位的扫描,从而获得原始的模型数据。

      1617944-20190627164319181-887459382.jpg

      上图展示了模型扫描仪,由很多摄影和灯光设备组成的球形矩阵。

    • 模型调整。由扫描阶段获取的初始模型通常有瑕疵,无法直接投入渲染。需要美术人员利用3D建模工具(如Maya、3DMax等)进行调整、优化、重新拓扑,最终调整成合适的可用模型。

      1617944-20190627164334353-1560131859.jpg

      左:扫描的初始模型;中:调整后的中间模型;右:优化了细节的可用模型。

    • 制作贴图。在此阶段,用建模软件或材质制作软件(如Substance)采纳高精度模型烘焙或制作出漫反射、法线、粗糙度、AO、散射、高光等等贴图,为最后的渲染做准备。这些贴图的原始尺寸通常都非常大,4K、8K甚至16K,目的是高精度还原虚拟人类的细节。

      1617944-20190627164347873-634855924.jpg

      漫反射贴图

      1617944-20190627164404125-1671208929.jpg

      法线贴图

    • 导入引擎。在此阶段,将之前制作的模型和贴图导入到渲染引擎(如UE4、Unity等),加入光照、材质、场景等元素,结合角色的综合性PBR渲染技术,获得最终成像。

      1617944-20190627164419772-239234264.jpg

      Unreal Engine渲染出的虚拟角色

    1.2 Unreal Engine的数字人类

    1.2.1 Unreal Engine数字人的历史

    Unreal Engine作为商业渲染引擎的巨头,在实时领域渲染数字人类做了很多尝试,关键节点有:

    • 2015年:《A Boy and His Kite》。展示了当时的开放世界概念和自然的角色动画风格与凭借第一人称射击游戏成名的Epic以前做过的任何项目都大不相同。

      1617944-20190627164445590-82540010.jpg

      《A Boy and His Kite》的画面

    • 2016年:《地狱之刃:塞娜的献祭》。这是Unreal将数字人引入实时游戏的一次尝试,从画质表现上,已经达到了异常逼真的程度。

      1617944-20190627164505453-2016411293.jpg

      《地狱之刃:塞娜的献祭》中的游戏角色画面

    • 2017年:《Meet Mike》。在Siggraph 2017中,Epic Game凭借此项目为世人展示了数字人科技的最新研究:利用最先进的画面捕捉技术、体感控制技术以及画面渲染技术在计算机中塑造人类的化身。其中数字人Mike是著名电影特效大师以及Fx Guide网站创始人Mike Seymour的化身。

      1617944-20190627164524243-294395705.png

      Unreal Engine官方团队制作的Mike虚拟角色

    • 2018年:《Siren》。Siren是Epic Game、3Lateral、Cubic Motion、Vicon以及腾讯的NEXT工作室等多家跨国公司倾力合作,花费半年多打造的顶级实时渲染的虚拟角色。从画质效果上看,已经与数码照片无异。

      1617944-20190627164539511-481130856.png

      《Siren》虚拟角色的细节,与数码相机摄制的照片如出一辙

    1.2.2 《Meet Mike》项目

    笔者本想以《Siren》的虚拟角色为依托进行研究,奈何官方并未将此项目开源。

    所以本文只能用《Meet Mike》项目的角色作为研究对象。

    《Meet Mike》项目的资源和源码可以从Unreal Engine的Epic Games Launcher中下载获得。

    1617944-20190627164556874-469047978.png

    《Meet Mike》资源和源码下载具体步骤

    若成功下载了Mike工程,打开项目的DigitalHuman.uproject文件,可以看到下面的画面:

    1617944-20190627164609363-925006470.png

    点击右上角World Outliner面板的”final_mike“,可以查看Mike模型及其所有材质的细节。

    1617944-20190627164633645-148323802.png

    如果要研究某个部分的材质(比如皮肤),双击对应的材质,即可打开材质节点。下图是双击M_Head皮肤材质后的界面:

    1617944-20190627164653283-1139879496.png

    打材质编辑器后,便可以进行后续的研究。后面章节将着重研究数字人的皮肤、眼球、毛发以及身体其它部位的渲染技术。

    Mike的一些数据:

    • 57万个三角形,69万个顶点。其中大量三角形集中在脸部,特别是头发,约占75%。

      1617944-20190627164710324-1411277609.png

    • 每根头发都是单独三角形,大约有2万多根头发。

      1617944-20190627164720906-1314145713.png

    • 脸部骨骼绑定使用了大约80个关节,大部分是为了头发的运动和脸部毛发。

    • 脸部模型大约只用了10个关节,分别用在下巴、眼睛和舌头,目的是为了运动更加圆滑。
    • 脸部使用了Technoprop公司先进的配有立体红外摄像头的固定在头部的面部捕捉装置。

    • 综合使用了750个融合变形(blend shapes)。

    • 系统使用了复杂的传统软件和三种深度学习AI引擎。

    二、皮肤渲染

    皮肤渲染技术经过数十年的发展,由最初的单张贴图+伦勃朗的渲染方式到近期的基于物理的SSSSS(屏幕空间次表面散射)。由此衍生出的皮肤渲染技术层出不穷,其中最最基础也最具代表性的是次表面散射(SSS)。

    在虚拟角色渲染中,皮肤的渲染尤为关键。因为皮肤是人们每天亲眼目睹的非常熟悉的东西,如果稍微渲染不好或细节处理不足,便会陷入恐怖谷(Uncanny Valley )理论。至于什么是恐怖谷理论,参看这里

    1617944-20190627164745650-806368084.jpg

    上图由于皮肤的细节处理不到位,陷入了恐怖谷理论

    2.1 皮肤的构成和理论

    2.1.1 皮肤构成

    人类皮肤的物理构成非常复杂,其表层和内部都由非常复杂的构成物质,剖面图如下:

    1617944-20190627164757111-1011619862.jpg

    • 绒毛(hair shaft)。附着于皮肤表面的细小的毛。

    • 油脂(oil)。皮肤表层有一层薄薄的油脂覆盖,是皮肤高光的主要贡献者。

    • 表皮(epidermis)。油脂层下是表皮覆盖,是造成次表面散射的物质之一。

    • 真皮(dermis)。表皮下面是真正的皮肤组织,也是造成次表面散射的物质之一。

    • 毛囊(hair follicle)。绒毛的皮下组织和根基。

    • 静脉(vein)。呈深蓝色的血管。

    • 动脉(artery)。呈暗红色的血管。

    • 脂肪组织(fatty tissue)。脂肪组织也是造成次表面散射的次要贡献物质。

    • 其它:皮肤表面的纹理、皱纹、毛孔、雀斑、痘痘、黑痣、疤痕、油脂粒等等细节。

      1617944-20190627164806555-645922944.jpg

      真实皮肤包含了非常多的细节:毛孔、绒毛、痘痘、黑痣、油脂......

    2.1.2 皮肤建模

    皮肤表面油脂层主要贡献了皮肤光照的反射部分(约6%的光线被反射),而油脂层下面的表皮层和真皮层则主要贡献了的次表面散射部分(约94%的光线被散射)。

    虽然皮肤构成非常复杂,但图形渲染界的先贤者们利用简化的思维将皮肤建模成若干层。

    1617944-20190627164818080-715291738.png

    • 表面油脂层(Thin Oily Layer):模拟皮肤的高光反射。
    • 表皮层(Epidermis):模拟次表面散射的贡献层。
    • 真皮层(Dermis):模拟次表面散射的贡献层。

    以上展示的是BRDF建模方式,只在皮肤表面反射光线,但实际上在三层建模中,还会考虑表皮层和真皮层的次表面散射(BSSRDF),见下图中间部分BSSRDF。

    1617944-20190627164828697-259065368.png

    2.1.3 皮肤渲染流程

    皮肤渲染涉及的高级技术有:

    皮肤渲染的过程可以抽象成以下步骤:

    • 皮肤反射。

      • 直接反射部分采用Cook-Torrance的BRDF,公式:
        \[ f_{cook-torrance} = \frac {D(h)F(l,h)G(l,v,h)}{4(n\cdot l)(n\cdot v)} \]
        具体解析和实现请参看《由浅入深学习PBR的原理和实现》的章节3.1.3 反射方程

        UE的皮肤渲染采用双镜叶高光(Dual Lobe Specular)。双镜叶高光度为两个独立的高光镜叶提供粗糙度值,二者组合后形成最终结果。当二者组合后,会为皮肤提供非常出色的亚像素微频效果,呈现出一种自然面貌。

        1617944-20190627164843333-1769195219.png

        其中UE默认的混合公式是:
        \[ Lobe1 \cdot 0.85 \ + \ Lobe2 \cdot 0.15 \]
        下图显示了UE4混合的过程和最终成像。

        1617944-20190627164900300-930368154.png

        左:较柔和的高光层Lobe1; 中:较强烈的高光层Lobe2; 右:最终混合成像

      • 非直接反射部分采用预卷积的cube map。

        具体解析和实现请参看《由浅入深学习PBR的原理和实现》的章节3.3.2 镜面的IBL(Specular IBL)

    • 皮肤毛孔。

      皮肤毛孔内部构造非常复杂,会造成反射(高光和漫反射)、阴影、遮挡、次表面散射等效应。

      1617944-20190627164911270-300948506.png

      人类毛孔放大图,内部构造异常复杂,由此产生非常复杂的光照信息

      在渲染毛孔细节时,需注意很多细节,否则会渲染结果陷入恐怖谷理论。

      理论上,接近物理真实的渲染,毛孔的渲染公式如下:
      \[ cavity \cdot Specular(gloss) \cdot Fresnel(reflectance) \]
      其中:

      • \(cavity\)是凹陷度。可从cavity map(下图)中采样获得。

        1617944-20190627164922918-618630861.jpg

      • \(Specular(gloss)\)表明高光项。

      • \(Fresnel(reflectance)\)是与视觉角度相关的反射。

      然而,这种物理真实,使得凹陷太明显,视觉不美观,有点让人不适:

      1617944-20190627164940718-915031853.png

      尝试微调高光和cavity的位置,可获得下面的渲染结果:

      1617944-20190627164957397-539932834.png

      上图可以看出,高光太强,凹陷细节不足,也是不够真实的皮肤渲染结果。

      实际上,可摒弃完全物理真实的原理,采用近似法:
      \[ Specular(gloss) \cdot Fresnel(cavity \cdot reflectance) \]
      最终可渲染出真实和美观相平衡的画面:

      1617944-20190627165013042-2086872201.png

      UE4采用漫反射+粗糙度+高光度+散射+法线等贴图结合的方式,以高精度还原皮肤细节。

      1617944-20190627165027677-88727808.png

      从左到右:漫反射、粗糙度、高光度、散射、法线贴图

      具体光照过程跟Cook-Torrance的BRDF大致一样,这里不详述。

    • 全局光照。

      皮肤的全局光照是基于图像的光照(IBL)+改进的AO结合的结果。

      其中IBL技术请参看3.3 基于图像的光照(Image Based Lighting,IBL)

      1617944-20190627165050974-32555556.png

      上图:叠加了全局光照,但无AO的画面

      AO部分是屏幕空间环境光遮蔽(SSAO),其中AO贴图混合了Bleed Color(皮肤通常取红色)。

      1617944-20190627165110637-760563636.png

      增加了红色Bleed Color的AO,使得皮肤渲染更加贴切,皮肤暗处的亮度和颜色更真实美观。

    • 次表面散射(BSSRDF)。

      这部分内容将在2.2更详细描述。

    2.2 次表面散射

    次表面散射(Subsurface scattering)是模拟皮肤、玉石、牛奶等半透光性物质的一种物理渲染技术。

    1617944-20190627165130372-121818006.png

    它与普通BRDF的区别在于,同一条入射光进入半透光性物质后,会在内部经过多次散射,最终在入射点附近散射出若干条光线。

    由于R、G、B在物质内扩散的曲线不一样,由此产生了与入射光不一样的颜色。

    1617944-20190627165143698-871981281.jpg

    红色光由于穿透力更强,更容易在皮肤组织穿透,形成红色光。

    2.2.1 BSSRDF

    BSSRDF是基于次表面散射的一种光照模型,充分考虑了入射光在物质内部经过若干次散射后重新反射出来的光。

    1617944-20190627165158073-397003102.png

    左:BRDF;右:BSSRDF,考虑了输入光在物质内散射后重新射出的若干条光

    1617944-20190627165209208-1107746198.jpg

    上图描述了BRDF、BTDF、BSSRDF之间的关系:

    • BRDF:双向反射分布函数,用于表述在介质入射点的反射光照模型。
    • BTDF:双向透射分布函数,用于描述光线透过介质后的光照模型。
    • BSSRDF:双向次表面反射分布函数,用于描述入射光在介质内部的光照模型。
      • BSDF = BRDF + BTDF。
      • BSSRDF是BSDF的升级版。

    下面两图展示了使用BRDF和BSSRDF的皮肤渲染结果:

    1617944-20190627165306736-1057688480.png

    BRDF光照模型渲染的皮肤

    1617944-20190627165319720-1765344845.png

    BSSRDF光照模型渲染的皮肤

    可见BSSRDF渲染的皮肤效果更真实,更美观,防止陷入恐怖谷效应。

    回顾一下BRDF的方程,它是一次反射光照的计算是在光线交点的法线半球上的球面积分:
    \[ L_o(p,\omega_o) = \int\limits_{\Omega} f_r(p,\omega_i,\omega_o) L_i(p,\omega_i) n \cdot \omega_i d\omega_i \]
    对于BSSRDF来说,每一次反射在物体表面上每一个位置都要做一次半球面积分,是一个嵌套积分:
    \[ L_o(p_o,\omega_o) = \int\limits_{A} \int\limits_{\Omega} S(p_o,\omega_o,p_i,\omega_i) L_i(p_i,\omega_i) n \cdot \omega_i d\omega_i dA \]
    \(S(p_o,\omega_o,p_i,\omega_i)\)项表明了次表面散射的计算过程,具体公式:
    \[ \begin{eqnarray} S(p_o,\omega_o,p_i,\omega_i) &\stackrel {def}{=}& \frac{dL_r(p_o,\omega_o)}{d\Phi_r(p_i,\omega_i)} \\ &=& \frac{1}{\pi}F_t(p_o,\omega_o)R_d(\parallel p_i-p_o\parallel)F_t(p_i,\omega_i) \\ \end{eqnarray} \]
    其中:

    • \(\frac{dL_r(p_o,\omega_o)}{d\Phi_r(p_i,\omega_i)}\)表明BSSRDF的定义是出射光的辐射度和入射通量的比值。

    • \(F_t\)是菲涅尔透射效应。

    • \(R_d(\parallel p_i-p_o\parallel)\)是扩散反射(Diffuse reflectance),与入射点和出射点的距离相关。
      \[ R_d(\parallel p_i-p_o\parallel) = -D\frac{(n\cdot \triangle\phi(p_o))}{d\Phi_i(p_i)} \]

      • \(D\)是漫反射常量:
        \[ D=\frac{1}{3\sigma_t'} \]

    由此可见,\(S\)项的计算过程比较复杂,对于实时渲染,是几乎不可能完成的。由此可采用近似法求解:
    \[ S(p_o,\omega_o,p_i,\omega_i) \approx (1-F_r(\cos\theta_o))S_p(p_o,p_i)S_\omega(\omega_i) \]
    其中:

    • \(F_r(\cos\theta_o)\)是菲涅尔反射项。

    • \(S_p(p_o,p_i)\)是点\(p\)处的次表面散射函数。它可以进一步简化:
      \[ S_p(p_o,p_i) \approx S_r(\parallel p_o - p_i\parallel) \]
      也就是说点\(p\)处的次表面系数只由入射点\(p_i\)和出射点\(p_o\)相关。

      \(S_r\)跟介质的很多属性有关,可用公式表达及简化:
      \[ \begin{eqnarray} S_r(\eta,g,\rho,\sigma_t,r) &=& \sigma^2_t S_r(\eta,g,\rho,1,r_{optical}) \\ &\approx& \sigma^2_t S_r(\rho,r_{optical}) \\ r_{optical} &=& \rho_t r \end{eqnarray} \]
      简化后的\(S_r\)只跟\(\rho\)\(r\)有关,每种材料的\(\rho\)\(r\)可组成一个BSSRDF表。

      1617944-20190627165335607-224944879.png

      上图展示了\(\rho=0.2\)\(r=0.5\)的索引表。

      通过\(\rho\)\(r\)可查询到对应的\(S_r\),从而化繁为简,实现实时渲染的目标。

    • \(S_\omega(\omega_i)\)是有缩放因子的菲涅尔项,它的公式:
      \[ S_\omega(\omega_i) = \frac{1-F_r(\cos\theta_i)}{c\cdot \pi} \]
      其中\(c\)是一个嵌套的半球面积分:
      \[ \begin{eqnarray} c &=& \int_0^{2\pi} \int_0^{\frac{\pi}{2}} \frac{1-F_r(\eta,\cos\theta)}{\pi}\sin\theta \ \cos\theta \ d\theta \ d\phi \\ &=& 1 - 2 \int_0^{\frac{\pi}{2}} F_r(\eta,\cos\theta)\sin\theta \ \cos\theta \ d\theta \ d\phi \end{eqnarray} \]

    BSSRDF公式更具体的理论、推导、简化过程可参看下面两篇论文:

    2.2.2 次表面散射的空间模糊

    次表面散射本质上是采样周边像素进行加权计算,类似特殊的高斯模糊。也就是说,次表面散射的计算可以分为两个部分:

    (1)先对每个像素进行一般的漫反射计算。

    (2)再根据某种特殊的函数\(R(r)\)和(1)中的漫反射结果,加权计算周围若干个像素对当前像素的次表面散射贡献。

    上述(2)中提到的\(R(r)\)就是次表面散射的扩散剖面(Diffusion Profile)。它是一个次表面散射的光线密度分布,是各向同性的函数,也就是说一个像素受周边像素的光照影响的比例只和两个像素间的距离有关

    实际上所有材质都存在次表面散射现象,区别只在于其密度分布函数\(R(r)\)的集中程度,如果该函数的绝大部分能量都集中在入射点附近(r=0),就表示附近像素对当前像素的光照贡献不明显,可以忽略,则在渲染时我们就用漫反射代替,如果该函数分布比较均匀,附近像素对当前像素的光照贡献明显,则需要单独计算次表面散射

    利用扩散剖面技术模拟的次表面散射,为了得到更柔和的皮肤质感,需要对画面进行若干次不同参数的高斯模糊。从模糊空间划分,有两种方法:

    • 纹理空间模糊(Texture Space Blur)。利用皮肤中散射的局部特性,通过使用纹理坐标作为渲染坐标展开3D网格,在2D纹理中有效地对其进行模拟。

    • 屏幕空间模糊(Screen Space Blur)。跟纹理空间不同的是,它在屏幕空间进行模糊,也被称为屏幕空间次表面散射(Screen Space SubSurface Scattering,SSSSS)。

      1617944-20190627165405680-708035397.png

      纹理空间和屏幕空间进行0, 3, 5次高斯模糊的结果

    1617944-20190627165457957-1392066657.jpg

    上图:屏幕空间的次表面散射渲染过程

    2.2.3 可分离的次表面散射(Separable Subsurface Scattering)

    次表面散射的模糊存在卷积分离(Separable Convolution)的优化方法,具体是将横向坐标U和纵向坐标V分开卷积,再做合成:

    1617944-20190627165511905-565073442.png

    由此产生了可分离的次表面散射(Separable Subsurface Scattering,也叫SSSS或4S),这也是UE目前采用的人类皮肤渲染方法。它将\(R_d\)做了简化:
    \[ R_d(x,y) \approx A_g(x,y) = \sum_{i=1}^N \omega_i G(x,y,\sigma_i) \]
    具体的推导过程请参看:Separable Subsurface Scattering

    该论文还提到,为了给实时渲染加速,还需要预积分分离的卷积核(Pre-integrated Separable Kernel):
    \[ A_p(x,y) = \frac{1}{\parallel R_d \parallel_1} a_p(x)a_p(y) \]
    利用奇异值分解(Singular Value Decomposition,SVD)的方法将其分解为一个行向量和一个列向量并且保证了分解后的表示方法基本没有能量损失。下图展示了它的计算过程:

    1617944-20190627165523449-878732361.png

    2.3 UE底层实现

    本节将从UE的C++和shader源码分析皮肤渲染的实现。UE源码下载的具体步骤请看官方文档:下载虚幻引擎源代码

    再次给拥有充分共享精神的Epic Game点个赞!UE的开源使我们可以一窥引擎内部的实现,不再是黑盒操作,也使我们有机会学习图形渲染的知识,对个人、项目和公司都大有裨益。

    皮肤渲染的方法很多,UE使用的是可分离的次表面散射(Separable Subsurface Scattering,也叫SSSS或4S)。最先由暴雪的Jorge等人,在GDC2013的演讲《Next-Generation Character Rendering》中首次展示了SSSS的渲染图,并在2015年通过论文正式提出了Separable Subsurface Scattering。其通过水平和垂直卷积2个Pass来近似,效率更进一步提升,这是目前游戏里采用的主流技术。

    UE源码中,与SSSS相关的主要文件(笔者使用的是UE 4.22,不同版本可能有所差别):

    • \Engine\Shaders\Private\SeparableSSS.ush:

      SSSS的shader主要实现。

    • \Engine\Shaders\Private\PostProcessSubsurface.usf:

      后处理阶段为SeparableSSS.ush提供数据和工具接口的实现。

    • \Engine\Shaders\Private\SubsurfaceProfileCommon.ush:

      定义了SSSS的常量和配置。

    • \Engine\Source\Runtime\Engine\Private\Rendering\SeparableSSS.cpp:

      实现CPU版本的扩散剖面、高斯模糊及透射剖面等逻辑,可用于离线计算。

    • \Engine\Source\Runtime\Engine\Private\Rendering\SubsurfaceProfile.cpp:

      SSS Profile的管理,纹理的创建,及与SSSS交互的处理。

    2.3.1 SeparableSSS.ush

    SeparableSSS.ush是实现SSSS的主要shader文件,先分析像素着色器代码。(下面有些接口是在其它文件定义的,通过名字就可以知道大致的意思,无需关心其内部实现细节也不妨碍分析核心渲染算法。)

    // BufferUV: 纹理坐标,会从GBuffer中取数据;
    // dir: 模糊方向。第一个pass取值float2(1.0, 0.0),表示横向模糊;第二个pass取值float2(0.0, 1.0),表示纵向模糊。这就是“可分离”的优化。
    // initStencil:是否初始化模板缓冲。第一个pass需要设为true,以便在第二个pass获得优化。
    float4 SSSSBlurPS(float2 BufferUV, float2 dir, bool initStencil)
    {
        // Fetch color of current pixel:
        // SSSSSampleSceneColorPoint和SSSSSampleSceneColor就是获取2.2.2步骤(1)中提到的已经计算好的漫反射颜色
        float4 colorM = SSSSSampleSceneColorPoint(BufferUV);
    
        // we store the depth in alpha
        float OutDepth = colorM.a;
    
        colorM.a = ComputeMaskFromDepthInAlpha(colorM.a);
    
        // 根据掩码值决定是否直接返回,而不做后面的次表面散射计算。
        BRANCH if(!colorM.a)
        {
            // todo: need to check for proper clear
    //      discard;
            return 0.0f;
        }
    
        // 0..1
        float SSSStrength = GetSubsurfaceStrength(BufferUV);
    
        // Initialize the stencil buffer in case it was not already available:
        if (initStencil) // (Checked in compile time, it's optimized away)
            if (SSSStrength < 1 / 256.0f) discard;
    
        float SSSScaleX = SSSParams.x;
        float scale = SSSScaleX / OutDepth;
    
        // 计算采样周边像素的最终步进
        float2 finalStep = scale * dir;
    
        // ideally this comes from a half res buffer as well - there are some minor artifacts
        finalStep *= SSSStrength; // Modulate it using the opacity (0..1 range)
        
        FGBufferData GBufferData = GetGBufferData(BufferUV);
    
        // 0..255, which SubSurface profile to pick
        // ideally this comes from a half res buffer as well - there are some minor artifacts
        uint SubsurfaceProfileInt = ExtractSubsurfaceProfileInt(GBufferData);
    
        // Accumulate the center sample:
        float3 colorAccum = 0;
        // 初始化为非零值,是为了防止后面除零异常。
        float3 colorInvDiv = 0.00001f;
    
        // 中心点采样
        colorInvDiv += GetKernel(SSSS_N_KERNELWEIGHTOFFSET, SubsurfaceProfileInt).rgb;
        colorAccum = colorM.rgb * GetKernel(SSSS_N_KERNELWEIGHTOFFSET, SubsurfaceProfileInt).rgb;
        
        // 边界溢色。
        float3 BoundaryColorBleed = GetProfileBoundaryColorBleed(GBufferData);
    
        // 叠加周边像素的采样,即次表面散射的计算,也可看做是与距离相关的特殊的模糊
        // SSSS_N_KERNELWEIGHTCOUNT是样本数量,与配置相关,分别是6、9、13。可由控制台命令r.SSS.SampleSet设置。
        SSSS_UNROLL
        for (int i = 1; i < SSSS_N_KERNELWEIGHTCOUNT; i++) 
        {
            // Kernel是卷积核,卷积核的权重由扩散剖面(Diffusion Profile)确定,而卷积核的大小则需要根据当前像素的深度(d(x,y))及其导数(dFdx(d(x,y))和dFdy(d(x,y)))来确定。并且它是根据Subsurface Profile参数预计算的。
            // Kernel.rgb是颜色通道的权重;Kernel.a是采样位置,取值范围是0~SUBSURFACE_KERNEL_SIZE(即次表面散射影响的半径)
            half4 Kernel = GetKernel(SSSS_N_KERNELWEIGHTOFFSET + i, SubsurfaceProfileInt);
    
            float4 LocalAccum = 0;
    
            float2 UVOffset = Kernel.a * finalStep;
            
            // 由于卷积核是各向同性的,所以可以简单地取采样中心对称的点的颜色进行计算。可将GetKernel调用降低至一半,权重计算消耗降至一半。
            SSSS_UNROLL
            // Side的值是-1和1,通过BufferUV + UVOffset * Side,即可获得采样中心点对称的两点做处理。
            for (int Side = -1; Side <= 1; Side += 2)
            {
                // Fetch color and depth for current sample:
                float2 LocalUV = BufferUV + UVOffset * Side;
                float4 color = SSSSSampleSceneColor(LocalUV);
                uint LocalSubsurfaceProfileInt = SSSSSampleProfileId(LocalUV);
                float3 ColorTint = LocalSubsurfaceProfileInt == SubsurfaceProfileInt ? 1.0f : BoundaryColorBleed;
    
                float LocalDepth = color.a;
                
                color.a = ComputeMaskFromDepthInAlpha(color.a);
    
    #if SSSS_FOLLOW_SURFACE == 1
                // 根据OutDepth和LocalDepth的深度差校正次表面散射效果,如果它们相差太大,几乎无次表面散射效果。
                float s = saturate(12000.0f / 400000 * SSSParams.y *
        //        float s = saturate(300.0f/400000 * SSSParams.y *
                    abs(OutDepth - LocalDepth));
    
                color.a *= 1 - s;
    #endif
                // approximation, ideally we would reconstruct the mask with ComputeMaskFromDepthInAlpha() and do manual bilinear filter
                // needed?
                color.rgb *= color.a * ColorTint;
    
                // Accumulate left and right 
                LocalAccum += color;
            }
    
            // 由于中心采样点两端的权重是对称的,colorAccum和colorInvDiv本来都需要*2,但它们最终colorAccum / colorInvDiv,所以*2可以消除掉。
            colorAccum += Kernel.rgb * LocalAccum.rgb;
            colorInvDiv += Kernel.rgb * LocalAccum.a;
        }
    
        // 最终将颜色权重和深度权重相除,以规范化,保持光能量守恒,防止颜色过曝。(对于没有深度信息或者没有SSS效果的材质,采样可能失效!)
        float3 OutColor = colorAccum / colorInvDiv; 
    
        // alpha stored the SceneDepth (0 if there is no subsurface scattering)
        return float4(OutColor, OutDepth);
    }

    此文件还有SSSSTransmittance,但笔者搜索了整个UE的源代码工程,似乎没有被用到,所以暂时不分析。下面只贴出其源码:

    //-----------------------------------------------------------------------------
    // Separable SSS Transmittance Function
    
    // @param translucency This parameter allows to control the transmittance effect. Its range should be 0..1. Higher values translate to a stronger effect.
    // @param sssWidth this parameter should be the same as the 'SSSSBlurPS' one. See below for more details.
    // @param worldPosition Position in world space.
    // @param worldNormal Normal in world space.
    // @param light Light vector: lightWorldPosition - worldPosition.
    // @param lightViewProjection Regular world to light space matrix.
    // @param lightFarPlane Far plane distance used in the light projection matrix.
    
    float3 SSSSTransmittance(float translucency, float sssWidth, float3 worldPosition, float3 worldNormal, float3 light, float4x4 lightViewProjection, float lightFarPlane)
     {
        /**
         * Calculate the scale of the effect.
         */
        float scale = 8.25 * (1.0 - translucency) / sssWidth;
           
        /**
         * First we shrink the position inwards the surface to avoid artifacts:
         * (Note that this can be done once for all the lights)
         */
        float4 shrinkedPos = float4(worldPosition - 0.005 * worldNormal, 1.0);
    
        /**
         * Now we calculate the thickness from the light point of view:
         */
        float4 shadowPosition = SSSSMul(shrinkedPos, lightViewProjection);
        float d1 = SSSSSampleShadowmap(shadowPosition.xy / shadowPosition.w).r; // 'd1' has a range of 0..1
        float d2 = shadowPosition.z; // 'd2' has a range of 0..'lightFarPlane'
        d1 *= lightFarPlane; // So we scale 'd1' accordingly:
        float d = scale * abs(d1 - d2);
    
        /**
         * Armed with the thickness, we can now calculate the color by means of the
         * precalculated transmittance profile.
         * (It can be precomputed into a texture, for maximum performance):
         */
        float dd = -d * d;
        float3 profile = float3(0.233, 0.455, 0.649) * exp(dd / 0.0064) +
                         float3(0.1,   0.336, 0.344) * exp(dd / 0.0484) +
                         float3(0.118, 0.198, 0.0)   * exp(dd / 0.187)  +
                         float3(0.113, 0.007, 0.007) * exp(dd / 0.567)  +
                         float3(0.358, 0.004, 0.0)   * exp(dd / 1.99)   +
                         float3(0.078, 0.0,   0.0)   * exp(dd / 7.41);
    
        /** 
         * Using the profile, we finally approximate the transmitted lighting from
         * the back of the object:
         */
        return profile * saturate(0.3 + dot(light, -worldNormal));
    }

    2.3.2 SeparableSSS.cpp

    SeparableSSS.cpp主题提供了扩散剖面、透射剖面、高斯模糊计算以及镜像卷积核的预计算。

    为了更好地理解源代码,还是先介绍一些前提知识。

    2.3.2.1 高斯和的扩散剖面(Sum-of-Gaussians Diffusion Profile)

    扩散剖面的模拟可由若干个高斯和函数进行模拟,其中高斯函数的公式:
    \[ f_{gaussian} = e^{-r^2} \]
    下图是单个高斯和的扩散剖面曲线图:

    1617944-20190627165540767-990797649.jpg

    由此可见R、G、B的扩散距离不一样,并且单个高斯函数无法精确模拟出复杂的人类皮肤扩散剖面。

    实践表明多个高斯分布在一起可以对扩散剖面提供极好的近似。并且高斯函数是独特的,因为它们同时是可分离的和径向对称的,并且它们可以相互卷积来产生新的高斯函数。

    对于每个扩散分布\(R(r)\),我们找到具有权重\(\omega_i\)和方差\(v_i\)\(k\)个高斯函数:
    \[ R(r) \approx \sum_{i=1}^k\omega_iG(v_i,r) \]
    并且高斯函数的方差\(v\)有以下定义:
    \[ G(v, r) := \frac{1}{2\pi v} e^{\frac{-r^2}{2v}} \]
    可以选择常数\(\frac{1}{2v}\)使得\(G(v, r)\)在用于径向2D模糊时不会使输入图像变暗或变亮(其具有单位脉冲响应(unit impulse response))。

    对于大部分透明物体(牛奶、大理石等)用一个Dipole Profile就够了,但是对于皮肤这种拥有多层结构的材质,用一个Dipole Profile不能达到理想的效果,可以通过3个Dipole接近Jensen论文中的根据测量得出的皮肤Profile数据。

    实验发现,3个Dipole曲线可通过以下6个高斯函数拟合得到(具体的拟合推导过程参见:《GPU Gems 3》:真实感皮肤渲染技术总结):
    \[ \begin{eqnarray} R(r) &=& 0.233\cdot G(0.0064,r) + 0.1\cdot G(0.0484,r) + 0.118\cdot G(0.187,r) \\ &+& 0.113\cdot G(0.567,r) + 0.358\cdot G(1.99,r) + 0.078\cdot G(7.41,r) \end{eqnarray} \]
    上述公式是红通道Red的模拟,绿通道Green和蓝通道Blue的参数不一样,见下表:

    1617944-20190627165552944-378735285.jpg

    R、G、B通道拟合出的曲线有所不同(下图),可见R通道曲线的扩散范围最远,这也是皮肤显示出红色的原因。

    1617944-20190627165605689-1302210234.jpg

    2.3.2.2 源码分析

    首先分析SeparableSSS_Gaussian

    // 这个就是上一小节提到的G(v,r)的高斯函数,增加了FalloffColor颜色,对应不同颜色通道的值。
    inline FVector SeparableSSS_Gaussian(float variance, float r, FLinearColor FalloffColor)
    {
        FVector Ret;
    
        // 对每个颜色通道做一次高斯函数技术
        for (int i = 0; i < 3; i++)
        {
            float rr = r / (0.001f + FalloffColor.Component(i));
            Ret[i] = exp((-(rr * rr)) / (2.0f * variance)) / (2.0f * 3.14f * variance);
        }
    
        return Ret;
    }

    再分析SeparableSSS_Profile

    // 天啦噜,这不正是上一小节提到的通过6个高斯函数拟合得到3个dipole曲线的公式么?参数一毛一样有木有?
    // 其中r是次表面散射的最大影响距离,单位是mm,可由UE编辑器的Subsurface Profile界面设置。
    inline FVector SeparableSSS_Profile(float r, FLinearColor FalloffColor)
    {
        // 需要注意的是,UE4将R、G、B通道的参数都统一使用了R通道的参数,它给出的理由是FalloffColor已经包含了不同的值,并且方便模拟出不同肤色的材质。
        return  // 0.233f * SeparableSSS_Gaussian(0.0064f, r, FalloffColor) + // UE4屏蔽掉了第一个高斯函数,理由是这个是直接反射光,并且考虑了strength参数。(We consider this one to be directly bounced light, accounted by the strength parameter)
            0.100f * SeparableSSS_Gaussian(0.0484f, r, FalloffColor) +
            0.118f * SeparableSSS_Gaussian(0.187f, r, FalloffColor) +
            0.113f * SeparableSSS_Gaussian(0.567f, r, FalloffColor) +
            0.358f * SeparableSSS_Gaussian(1.99f, r, FalloffColor) +
            0.078f * SeparableSSS_Gaussian(7.41f, r, FalloffColor);
    }

    接着分析如何利用上面的接口进行离线计算Kernel的权重:

    // 由于高斯函数具体各向同性、中心对称性,所以横向卷积和纵向卷积一样,通过镜像的数据减少一半计算量。
    void ComputeMirroredSSSKernel(FLinearColor* TargetBuffer, uint32 TargetBufferSize, FLinearColor SubsurfaceColor, FLinearColor FalloffColor)
    {
        check(TargetBuffer);
        check(TargetBufferSize > 0);
    
        uint32 nNonMirroredSamples = TargetBufferSize;
        int32 nTotalSamples = nNonMirroredSamples * 2 - 1;
    
        // we could generate Out directly but the original code form SeparableSSS wasn't done like that so we convert it later
        // .A is in mm
        check(nTotalSamples < 64);
        FLinearColor kernel[64];
        {
            // 卷积核时先给定一个默认的半径范围,不能太大也不能太小,根据nTotalSamples数量调整Range是必要的。(单位是毫米mm)
            const float Range = nTotalSamples > 20 ? 3.0f : 2.0f;
            // tweak constant
            const float Exponent = 2.0f;
    
            // Calculate the offsets:
            float step = 2.0f * Range / (nTotalSamples - 1);
            for (int i = 0; i < nTotalSamples; i++)
            {
                float o = -Range + float(i) * step;
                float sign = o < 0.0f ? -1.0f : 1.0f;
                // 将当前的range和最大的Range的比值存入alpha通道,以便在shader中快速应用。
                kernel[i].A = Range * sign * FMath::Abs(FMath::Pow(o, Exponent)) / FMath::Pow(Range, Exponent);
            }
    
            // 计算Kernel权重
            for (int32 i = 0; i < nTotalSamples; i++)
            {
                // 分别取得i两边的.A值做模糊,存入area
                float w0 = i > 0 ? FMath::Abs(kernel[i].A - kernel[i - 1].A) : 0.0f;
                float w1 = i < nTotalSamples - 1 ? FMath::Abs(kernel[i].A - kernel[i + 1].A) : 0.0f;
                float area = (w0 + w1) / 2.0f;
                // 将模糊后的权重与6个高斯函数的拟合结果相乘,获得RGB的最终权重。
                FVector t = area * SeparableSSS_Profile(kernel[i].A, FalloffColor);
                kernel[i].R = t.X;
                kernel[i].G = t.Y;
                kernel[i].B = t.Z;
            }
    
            // 将offset为0.0(即中心采样点)的值移到位置0.
            FLinearColor t = kernel[nTotalSamples / 2];
    
            for (int i = nTotalSamples / 2; i > 0; i--)
            {
                kernel[i] = kernel[i - 1];
            }
            kernel[0] = t;
    
            // 规范化权重,使得权重总和为1,保持颜色能量守恒.
            {
                FVector sum = FVector(0, 0, 0);
    
                for (int i = 0; i < nTotalSamples; i++)
                {
                    sum.X += kernel[i].R;
                    sum.Y += kernel[i].G;
                    sum.Z += kernel[i].B;
                }
    
                for (int i = 0; i < nTotalSamples; i++)
                {
                    kernel[i].R /= sum.X;
                    kernel[i].G /= sum.Y;
                    kernel[i].B /= sum.Z;
                }
            }
    
            /* we do that in the shader for better quality with half res 
            
            // Tweak them using the desired strength. The first one is:
            //     lerp(1.0, kernel[0].rgb, strength)
            kernel[0].R = FMath::Lerp(1.0f, kernel[0].R, SubsurfaceColor.R);
            kernel[0].G = FMath::Lerp(1.0f, kernel[0].G, SubsurfaceColor.G);
            kernel[0].B = FMath::Lerp(1.0f, kernel[0].B, SubsurfaceColor.B);
    
            for (int i = 1; i < nTotalSamples; i++)
            {
                kernel[i].R *= SubsurfaceColor.R;
                kernel[i].G *= SubsurfaceColor.G;
                kernel[i].B *= SubsurfaceColor.B;
            }*/
        }
    
        // 将正向权重结果输出到TargetBuffer,删除负向结果。
        {
            check(kernel[0].A == 0.0f);
    
            // center sample
            TargetBuffer[0] = kernel[0];
    
            // all positive samples
            for (uint32 i = 0; i < nNonMirroredSamples - 1; i++)
            {
                TargetBuffer[i + 1] = kernel[nNonMirroredSamples + i];
            }
        }
    }

    此文件还实现了ComputeTransmissionProfile

    void ComputeTransmissionProfile(FLinearColor* TargetBuffer, uint32 TargetBufferSize, FLinearColor SubsurfaceColor, FLinearColor FalloffColor, float ExtinctionScale)
    {
        check(TargetBuffer);
        check(TargetBufferSize > 0);
    
        static float MaxTransmissionProfileDistance = 5.0f; // See MAX_TRANSMISSION_PROFILE_DISTANCE in TransmissionCommon.ush
    
        for (uint32 i = 0; i < TargetBufferSize; ++i)
        {
            //10 mm
            const float InvSize = 1.0f / TargetBufferSize;
            float Distance = i * InvSize * MaxTransmissionProfileDistance;
            FVector TransmissionProfile = SeparableSSS_Profile(Distance, FalloffColor);
            TargetBuffer[i] = TransmissionProfile;
            //Use Luminance of scattering as SSSS shadow.
            TargetBuffer[i].A = exp(-Distance * ExtinctionScale);
        }
    
        // Do this is because 5mm is not enough cool down the scattering to zero, although which is small number but after tone mapping still noticeable
        // so just Let last pixel be 0 which make sure thickness great than MaxRadius have no scattering
        static bool bMakeLastPixelBlack = true;
        if (bMakeLastPixelBlack)
        {
            TargetBuffer[TargetBufferSize - 1] = FLinearColor::Black;
        }
    }

    ComputeMirroredSSSKernelComputeTransmissionProfile的触发是在FSubsurfaceProfileTexture::CreateTexture内,而后者又是在关卡加载时或者编辑器操作时触发调用(也就是说预计算的,非运行时计算):

    void FSubsurfaceProfileTexture::CreateTexture(FRHICommandListImmediate& RHICmdList)
    {
        // ... (隐藏了卷积前的处理代码)
    
        for (uint32 y = 0; y < Height; ++y)
        {
            // ... (隐藏了卷积前的处理代码)
            
            // 根据r.SSS.SampleSet的数值(0、1、2),卷积3个不同尺寸的权重。
            ComputeMirroredSSSKernel(&TextureRow[SSSS_KERNEL0_OFFSET], SSSS_KERNEL0_SIZE, Data.SubsurfaceColor, Data.FalloffColor);
            ComputeMirroredSSSKernel(&TextureRow[SSSS_KERNEL1_OFFSET], SSSS_KERNEL1_SIZE, Data.SubsurfaceColor, Data.FalloffColor);
            ComputeMirroredSSSKernel(&TextureRow[SSSS_KERNEL2_OFFSET], SSSS_KERNEL2_SIZE, Data.SubsurfaceColor, Data.FalloffColor);
            
            // 计算透射剖面。
            ComputeTransmissionProfile(&TextureRow[SSSS_TRANSMISSION_PROFILE_OFFSET], SSSS_TRANSMISSION_PROFILE_SIZE, Data.SubsurfaceColor, Data.FalloffColor, Data.ExtinctionScale);
    
            // ...(隐藏了卷积后的处理代码)
        }
    }

    2.3.3 PostProcessSubsurface.ush

    此文件为SeparableSSS.ush定义了大量接口和变量,并且是调用SeparableSSS的使用者:

    // .... (隐藏其它代码)
    
    #include "SeparableSSS.ush"
    
    // .... (隐藏其它代码)
    
    // input0 is created by the SetupPS shader
    void MainPS(noperspective float4 UVAndScreenPos : TEXCOORD0, out float4 OutColor : SV_Target0)
    {
        float2 BufferUV = UVAndScreenPos.xy;
        
    #if SSS_DIRECTION == 0
        // horizontal
        float2 ViewportDirectionUV = float2(1, 0) * SUBSURFACE_RADIUS_SCALE;
    #else
        // vertical
        float2 ViewportDirectionUV = float2(0, 1) * SUBSURFACE_RADIUS_SCALE * (View.ViewSizeAndInvSize.x * View.ViewSizeAndInvSize.w);
    #endif
    
        #if MANUALLY_CLAMP_UV
            ViewportDirectionUV *= (View.ViewSizeAndInvSize.x * View.BufferSizeAndInvSize.z);
        #endif
        
        // 获得次表面散射颜色
        OutColor = SSSSBlurPS(BufferUV, ViewportDirectionUV, false);
    
    #if SSS_DIRECTION == 1
        // second pass prepares the setup from the recombine pass which doesn't need depth but wants to reconstruct the color
        OutColor.a = ComputeMaskFromDepthInAlpha(OutColor.a);
    #endif
    }

    并且在调用MainPS前,已经由其它代码计算好了漫反射颜色,后续还会进行高光混合。如果在预计算卷积核之前就混合了高光,会得到不好的渲染结果:

    1617944-20190627165621708-853241761.png

    2.3.4 UE次表面散射的限制

    UE4的次表面散射虽然能提高非常逼真的皮肤渲染,但也存在以下限制(摘自官方文档:次表面轮廓明暗处理模型):

    • 该功能不适用于非延迟(移动)渲染模式。

    • 将大屏幕设置为散射半径,将会在极端照明条件下显示出带状瑕疵。

      1617944-20190627165635625-1476833854.png

    • 目前,没有照明反向散射。

    • 目前,当非SSS材质遮挡SSS材质时,会出现灰色轮廓。(经笔者测试,4.22.1并不会出现,见下图)

      1617944-20190627165649356-141143188.png

    2.4 皮肤材质解析

    本节将开始解析Mike的皮肤材质。皮肤材质主要是M_Head。

    1617944-20190627165704740-1839707608.png

    皮肤材质节点总览

    它的启用了次表面散射的着色模型,此外,还开启了与骨骼动作和静态光一起使用标记,如下:

    1617944-20190627165724167-1039327751.png

    2.4.1 基础色(Base Color)

    1617944-20190627165736595-1323421829.png

    对于基础色,是由4张漫反射贴图(下图)作为输入,通过MF_AnimatedMapsMike输出混合的结果,再除以由一张次表面散射遮罩图(T_head_sss_ao_mask)控制的系数,最终输入到Base Color引脚。

    1617944-20190627165747520-1453341189.png

    4张漫反射贴图,每张都代表着不同动作状态下的贴图。

    其中MF_AnimatedMapsMike是一个通用的材质函数,内部控制着不同动作下的贴图混合权重,而混合不同动作参数的是m_headMask_01m_headMask_02m_headMask_03三个材质函数:

    1617944-20190627165807234-1575947053.png

    m_headMask_01m_headMask_02m_headMask_03三个材质函数又分别控制了一组面部Blend Shape动作,其中以m_headMask_01为研究对象:

    1617944-20190627165837611-1533390276.png

    由上图可见,m_headMask_01有5张贴图(head_wm1_msk_01 ~ head_wm1_msk_04,head_wm13_msk_03),利用它们的共19个通道(head_wm1_msk_04的alpha通道没用上)提供了19组blend shape遮罩,然后它们与对应的参数相作用。

    此外,m_headMask_02有3张贴图控制了10个Blend Shape动作;m_headMask_03有3张贴图控制了12个Blend Shape动作。

    至于遮罩数据和blend shape参数如何计算,还得进入fn_maskDelta_xx一探究竟,下面以fn_maskDelta_01为例:

    1617944-20190627165854196-1020639212.png

    不要被众多的材质节点搞迷糊了,其实就是将每个Blend Shape遮罩与参数相乘,再将结果与其它参数相加,最终输出结果。抽象成公式:
    \[ f = \sum_{i=1}^N m_i \cdot p_i \]
    其中\(m_i\)表示第\(i\)个Blend Shape的遮罩值,\(p_i\)表示第\(i\)个Blend Shape的参数值。奏是辣么简单!

    2.4.2 高光(Specular)

    1617944-20190627165911578-1653487198.png

    高光度主要由Mike_head_cavity_map_001的R通道提供,通过PowerLerp调整强度和范围后,再经过Fresnel菲涅尔节点增强角色边缘的高光反射(下图)。

    1617944-20190627165931539-936450232.png

    上述结果经过T_head_sss_ao_mask贴图的Alpha通道控制高光度和BaseSpecularValue调整后,最终输出到Specular引脚。(下图)

    1617944-20190627165941060-939835114.png

    其中鼻子区域的高光度通过贴图T_RGB_roughness_02的R通道在原始值和0.8之间做插值。

    2.4.3 粗糙度(Roughness)

    粗糙度的计算比较复杂,要分几个部分来分析。

    2.4.3.1 动作混合的粗糙度

    1617944-20190627165955580-1305205053.png

    这部分跟基础色类似,通过4张不同动作状态的粗糙度贴图(Toksvig_mesoNormal,Toksvig_mesoNormal1,Toksvig_mesoNormal2,Toksvig_mesoNormal3)混合成初始粗糙度值。

    2.4.3.2 基于微表面的粗糙度

    1617944-20190627170005357-652496122.png

    如上图,由Toksvig_mesoNormal的G通道加上基础粗糙度BaseRoughness,再进入材质函数MF_RoughnessRegionMult处理后输出结果。

    其中,MF_RoughnessRegionMult的内部计算如下:

    1617944-20190627170014589-492502161.png

    简而言之,就是通过3张mask贴图(head_skin_mask4,T_siren_head_roughmask_02,T_siren_head_roughmask_01)的10个通道分别控制10个部位的粗糙度,并且每个部位的粗糙度提供了参数调节,使得每个部位在\([1.0, mask]\)之间插值。

    2.4.3.3 粗糙度调整和边缘粗糙度

    1617944-20190627170024483-1949316802.png

    上图所示,RoughnessVariation通过Mike_T_specular_neutral的R通道,在Rough0Rough1之间做插值;EdgeRoughness则通过Fresnel节点加强了角色视角边缘的粗糙度;然后将它们和前俩小节的结果分别做相乘和相加。

    2.4.3.4 微表面细节加强

    1617944-20190627170036952-2088201205.png

    如上图,将纹理坐标做偏移后,采用微表面细节贴图skin_h,接着加强对比度,并将值控制在\([0.85, 1.0]\)之间,最后与上一小节的结果相乘,输出到粗糙度引脚。

    其中微表面细节贴图skin_h见下:

    1617944-20190627170045907-1113093941.png

    2.4.4 次表面散射(Opacity)

    首先需要说明,当材质着色模型是Subsurface Profile时,材质引脚Opacity的作用不再是控制物体的透明度,而变成了控制次表面散射的系数。

    1617944-20190627170055872-1005901705.png

    由贴图T_head_sss_ao_mask的G通道(下图)提供主要的次表面散射数据,将它们限定在[ThinScatter,ThickScatter]之间。

    1617944-20190627170105649-954917560.png

    次表面散射遮罩图。可见耳朵、鼻子最强,鼻子、嘴巴次之。

    另外,通过贴图T_RGB_roughness_02的B、A通道分别控制上眼睑(UpperLidScatter)和眼皮(LidScatter)部位的次表面散射系数。

    2.4.5 法线(Normal)

    1617944-20190627170119218-312036215.png

    与漫反射、粗糙度类似,法线的主要提供者也是由4张图控制。

    此外,还提供了微观法线,以增加镜头很近时的皮肤细节。

    1617944-20190627170127544-733246884.png

    主法线和微观法线分别经过NormalStrengthMicroNormalStrength缩放后(注意,法线的z通道数据不变),再通过材质节点BlendAngleCorrectedNormals将它们叠加起来,最后规范化输入到法线引脚。(见下图)

    1617944-20190627170135651-1063569223.png

    不妨进入材质节点BlendAngleCorrectedNormals分析法线的混合过程:

    1617944-20190627170147171-2050825436.png

    从材质节点上看,计算过程并不算复杂,将它转成函数:

    Vector3 BlendAngleCorrectedNormals(Vector3 BaseNormal, Vector3 AdditionalNormal)
    {
        BaseNormal.b += 1.0;
        AdditionalNormal.rg *= -1.0;
        float dot = Dot(BaseNormal, AdditionalNormal);
        Vector3 result = BaseNormal * dot - AdditionalNormal * BaseNormal.b;
        return result;
    }

    另外,Normal Map Blending in Unreal Engine 4一文提出了一种更简单的混合方法:

    1617944-20190627170156510-593239768.jpg

    将两个法线的XY相加、Z相乘即得到混合的结果。

    2.4.6 环境光遮蔽(Ambient Occlusion)

    1617944-20190627170206842-1136697477.png

    AO控制非常简单,直接用贴图T_head_sss_ao_mask的R通道输入到AO引脚。其中T_head_sss_ao_mask的R通道如下:

    1617944-20190627170216113-999412839.png

    可见,五官内部、下颚、脖子、头发都屏蔽了较多的环境光。

    2.5 皮肤贴图制作

    前面可以看到,皮肤渲染涉及的贴图非常多,多达几十张。

    1617944-20190627170229691-2088291731.png

    它们的制作来源通常有以下几种:

    • 扫描出的超高清贴图。例如漫反射、高光、SSS、粗糙度、法线等等。

    • 转置贴图。比如粗糙度、副法线、微观法线等。

      1617944-20190627170239475-617013033.png

      粗糙度贴图由法线贴图转置而成。

    • 遮罩图。这类图非常多,标识了身体的各个区域,以便精准控制它们的各类属性。来源有:

      • PS等软件制作。此法最传统,也最容易理解。

      • 插件生成。利用Blend Shape、骨骼等的权重信息,自动生成遮罩图。

        1617944-20190627170252411-1072946082.png

        Blend Shape记录了顶点的权重,可以将它们对应的UV区域生成遮罩图。

    特别说明

    • 本系列还有眼球、毛发、其它身体部位的分析,未完待续
    • 感谢参考文献的作者们。

    参考文献

    转载于:https://www.cnblogs.com/timlly/p/11098212.html

    展开全文
  • UE4引擎 性能查看及优化

    千次阅读 2018-08-30 10:20:47
    本文依据UE4官方文档以及官方博客等总结而来,可能不全面,后面会陆续添加。内置工具的详细说明请参考官方文档。 游戏帧率很低,或者有卡顿的现象,可能会有很多原因,这时候不要乱猜,比如是不是人物太多了或者...

    本文依据UE4官方文档以及官方博客等总结而来,可能不全面,后面会陆续添加。内置工具的详细说明请参考官方文档。

    游戏帧率很低,或者有卡顿的现象,可能会有很多原因,这时候不要乱猜,比如是不是人物太多了或者渲染的东西太多了,这样猜意义是不大的,可能会浪费很多时间,但是总找不到点上,当然如果运气好也可以找到瓶颈,这个时候我们可以借助相应的工具来查找性能瓶颈。此处我们仅以UE4来展开讲解。

    首先要确定瓶颈是在CPU还是GPU,为了找到是谁,以非debug版本启动你的程序,并且在控制台上输入stat unit 命令,如果是在android平台上可以同时按下四个手指,打开控制台,输入stat unit,它会显示如下图所示:

    Frame时间是产生一帧花的总时间,由于逻辑线程(Game)和渲染线程(Draw)在一帧结束的时候需要同步,一帧花的时间经常跟其中的一个线程花的时间 相近。GPU时间测量了显卡渲染当前场景花的时间。由于 GPU时间是跟当前帧同步的,所以它跟一帧花的时间也基本差不多。

    如果一帧花的时间跟逻辑线程的时间比较接近,那么瓶颈在逻辑线程,相反如果跟渲染线程的时间比较接近,那么瓶颈在渲染线程。如果两个时间 都不接近,但跟GPU时间比较接近,那么瓶颈在显卡上。

    当然也可以使用一些第三方工具,比如intel vtume,、aqtime等,移动平台上可以使用Apple Instruments、NVIDIA Tegra System Profiler、ARM DS-5等 。

    瓶颈在逻辑线程

    可以通过性能分析来确定,通过~打开控制台里面输入"stat startfile",让它运行一会至少10s来获取一个多帧的平均值。如果时长过长,那么生成的文件就会很大。通过stat stopfile来结束性能分析。一个后缀为ue4stats的文件会在工程的路径下产生,如果是android的话会在你安装的目录下面生成 一个profile目录。如果想要查看分析结果,必须把这个文件拷贝到pc上,可以使用adb pull {ue4stats 完整路径} {pc 保存路径}来拷贝文件到pc上。

    这个时候你就可以使用UnrealFrontEnd(跟UE4Editor在同级目录)来打开分析的结果,或者在UE4Edtior里面通过window-->Developper ToolsàSession Frontend,打开后切换到Profiler面板,通过load来打开ue4stats文件。

    当打开后你就可以自己来查看耗费时间的地方了

    如果要查看卡顿,可以在时间线上查看高峰的地方,通过选择Maximum而不是Average,这样它就会显示一些峰值,如下图所示。

    GPU分析

    如果是在PC平台上可以使用ProfileGPU命令或者使用快捷键Ctrl+Shift+,

    也可以使用一些第三方工具来测试,pc平台上如 Intel GPA、Nvidia NSight visual Studio edition,移动平台比如高通的adreno profiler、NVIDIA Tegra Graphics Debugger、ImgTec PVRTune and PVRTrace、ARM Mali Graphics Debugger等,苹果的XCode等均可以用来分析。

    一些常用的命令

    • stat unit

    • stat scenerendering

    • stat engine

    几个对分析最有用的变量:

    • r.SetRes    改变屏幕,或窗口的分辨率。
    • r.VSync    开启/关闭垂直同步(可能依赖于是否原生全屏)。
    • r.ScreenPercentage    用于减小内部实际渲染分辨率,画面会在重新放大。
    • r.AllowOcclusionQueries    用于禁用遮挡(可以让场景运行的更慢)。
    • r.TiledDeferredShading    能够关闭基于 Tile 的延迟光照技术(GPU粒子的光影则没有退回方法)。
    • r.TiledDeferredShading.MinimumCount    能够调整使用多少灯光应用在基于 Tile 的延迟光照技术(视觉上并没有差异但性能会有不同)。
    • Pause    暂停游戏或者 Matinee(分析时更加稳定,但禁用了 Update/Tick)。
    • Slomo    能够对游戏进行加速或者减速播放。
    • r.VisualizeOccludedPrimitives    显示被裁剪掉的物件的外盒框。
    • StartFPSChart StopFPSChart    请看下文。
    • r.SeparateTranslucency    这是一个用于修复半透明情况下景深的问题的功能,如果不需要的时候可以把它关闭,并有其他影响(查阅 SceneColor)。
    • r.Tonemapper.GrainQuantization    用于关闭在 Tonemapper 中添加的噪点来避免 Color Banding,由于 8bit 量化和较小的质量改进在输出为 10:10:10 并不必须。
    • r.SceneColorFormat    能够选用不同的 SceneColor 格式(默认是 64bit 的最佳质量,并支持屏幕空间子表面散射)。
    • FX.AllowGPUSorting    禁用粒子排序(在大量粒子的使用可以妥协使用)。
    • FX.FreezeParticleSimulation    禁止粒子的更新。
    • r.SSR.MaxRoughness    调整屏幕空间反射(SSR)粗造度的最大值,并覆盖后处理中的该设置。请查阅 Show Flag VisualizeSSR。

    命令行选项

    有些功能可以在命令行中进行关闭,比如 UE4.exe –NoSound

    几个对分析比较有用的开关是:

    • -NoSound    禁用声音和音乐系统。
    • -NoTextureStreaming
    • 关闭贴图 steaming(对于隔离问题时很有帮助)。
    • -NoVerifyGC    否则需要预期在 Release 版本中每 30 秒会遇到的性能波动。
    • -NoVSync    能够更快的渲染但会导致画面撕裂,尤其是在高帧数下。
    • -Streaming    在使用 StartFPSChart/StopFPSChart 很有用,能够从一个非 windows 设备上来获取数据并用于进一步检测(假设我们是实时的 cook 数据)
    展开全文
  • 游戏引擎Unity&Unreal对比分析

    千次阅读 2019-10-29 15:34:26
    参考简书链接和百度百科:<...1、什么是游戏引擎        游戏引擎是指一些已编写好的可编辑电脑游戏系统或者一些交互式实时图像应用程序的核心组件。这些系统为...
  • 随着游戏内容的日趋精品化和重度化,Unreal引擎逐渐走进国内开发商的视野并被使用,然而其高门槛的操作使用和项目本身的重度性也使得优化工作愈发复杂艰难。作为性能优化的服务商,UWA测评今天正式支持了Unreal 引擎...
  • 探索开发一个引擎需要用到多少知识和技术
  • 虚幻4(UnrealEngine4)引擎源代码

    热门讨论 2020-07-30 23:33:23
    Epic Games今天宣布,正式发布“虚幻引擎4”(Unreal Engine 4/UE4),相关所有资源也一并放出。# h9 u6 O( p! y& D) q4 K7 P9 ~# N  只要每个月花19美元,你就能到全部资源,包括拿过来就能用的虚幻编辑器(Unreal ...
  • 官方 官方文档 ...中文版 http://api.unrealengine.com/CHN/ 英文版 https://docs.unrealengine.com/en-us/ 虚幻学院(英文教程,未来可能有汉化) https://academy.unrealengine.com/ 官方B站 ...
  • UnrealEngine3-渲染构架

    千次阅读 2015-08-02 21:22:44
    page covers all areas of the rendering subsystem in Unreal Engine 3. 本文覆盖了虚幻3引擎渲染子系统的方方面面。 Getting Started There's a lot of rendering code in UE3 so it's hard t
  • Unreal 代码流程分析

    千次阅读 2013-10-13 22:22:02
    WinMain 1 记录实例句柄及包名称 2 生成Log文件路径名称-存于Log.Filename 3 app初始化-appInit 3.1 文件管理器GFileManager 3.2 初始化 CRC 表-GCRCTable 3.3 初始化Log表-GLogs ...3.6
  • Unreal 入门-性能分析

    千次阅读 2017-03-23 00:00:10
    命令:stat unit 游戏的实际单帧时间由这三者之一限制:Game(CPU ... 游戏线程),Draw(CPU 渲染线程)或者 GPU(GPU)。... 图中我们可以看到 GPU是限制主因(三者最大的一个)。为了取得更少的 单帧 时间...
  • 2 游戏蛮牛相关资料的整理 :游戏蛮牛 - 手册 - 虚幻引擎4 | UnrealEngine4 二 深入了解虚幻底层原理 1 因为c++自身是没有垃圾回收的,所有虚幻4自己实现了一套垃圾回收机制 :虚幻4垃圾回收剖析 2 同样c++自身...
  • Unreal3游戏引擎UObject源码阅读分析

    千次阅读 2011-03-14 22:05:00
    Unreal3(以下简称U3)是一个以脚本为主体的游戏引擎,所有代码几乎都为脚本服务,意味着所有源码都会提供相应的脚本函数在脚本中使用。 UObject是U3中很重要的一个基类,所有由UObject派生的类都可以...
  • 开源自动驾驶仿真平台 AirSim (1) - Unreal EngineAirSim 官方Github: https://github.com/Microsoft/AirSimAirSim 是微软的开源自动驾驶仿真平台(其实它还能做很多...AirSim 使用了 Unreal Engine 作为自己的图像...
  • 从今天开始,我们准备将我们内部学习过的Unreal相关技术文档进行总结和整理,并陆续发布出来以方便中国更多的游戏开发者和引擎爱好者对Unreal引擎进行了解和学习。 同时,我们也要特别感谢Unreal中国团队对于本篇...
  • unreal应该算是现在最实用的开源渲染引擎之一了 基于物理的渲染(PBS)用起来真的是为所欲为 这里决定花点时间研究下unreal低层的渲染代码,这里假设读者具有基本的渲染知识,至少知道BRDF是什么 ...
  •  2:服务器有较高的容错性,对服务器状况有专业的记录和管理报表分析。  3:功能全面,使用非常方便,开发速度快。  4:支持无缝世界。  5:嵌入的Python脚本,开发非常方便。 缺点: 1:该引擎更适合制作FPS...
  • Unreal Engine 4 优化教程 第一部分

    千次阅读 2019-07-05 10:09:19
    本教程旨在帮助开发人员提升基于虚幻引擎(Unreal Engine*4 (UE4))开发的游戏性能。在教程中,我们对引擎内部及外部使用的一系列工具,以及面向编辑器的最佳实践加以概述,还提供了有助于提高帧速率和项目稳定性的...
1 2 3 4 5 ... 20
收藏数 1,997
精华内容 798
关键字:

unreal 引擎剖析