unreal 动作捕捉插件_unreal4 插件路径依赖引擎插件 - CSDN
精华内容
参与话题
  • 动捕设备用的是诺亦腾的,惯性设备多多少少会有些漂移,不过总体效果还行。目前数据传输采用的是Socket,不过后面会改成VRPN会更通用一些。


    动捕设备用的是诺亦腾的,惯性设备多多少少会有些漂移,不过总体效果还行。

    目前数据传输采用的是Socket,不过后面会改成VRPN会更通用一些。


    UE4的Editor之所以是白色的,是修改了里面的部分源代码。



    展开全文
  • Unity3D 简易细节层次插件 Simple LODhttp://www.idoubi.net/unity3d/tool/3764.htmlUnity3D 物体表面贴花喷漆插件 Easy Decal Easy Decal v1.6.8http://www.idoubi.net/unity3d/tool/4060.htmlUnity3D 汽车底盘传动...

    Unity3D 简易细节层次插件 Simple LOD
    http://www.idoubi.net/unity3d/tool/3764.html
    Unity3D 物体表面贴花喷漆插件 Easy Decal Easy Decal v1.6.8
    http://www.idoubi.net/unity3d/tool/4060.html
    Unity3D 汽车底盘传动模拟插件 Kinematic Car Suspension – Offroad Car
    http://www.idoubi.net/unity3d/tool/3162.html
    Unity3D A星寻路插件 A* Pathfinding Project Pro
    http://www.idoubi.net/unity3d/tool/1538.html
    Unity3D 山洞制作插件 MicroSplat – Terrain Holes v2.54
    http://www.idoubi.net/uncategorized/3154.html
    Unity3D 动态骨骼插件 Dynamic Bone
    http://www.idoubi.net/unity3d/tool/3733.html
    Unity3D 高级输入输出控制插件 Rewired v1.1.22.3
    http://www.idoubi.net/unity3d/tool/3785.html
    Unity3D 体积光和雾气制作插件 Aura 2
    http://www.idoubi.net/unity3d/tool/3769.html
    Unity3D 网格地形制作插件 Grids Pro
    http://www.idoubi.net/unity3d/tool/3426.html
    Unity3D 视图捕捉插件 Offline Render v1.0
    http://www.idoubi.net/unity3d/tool/3580.html
    Unreal 战争迷雾效果资源包 Fog of War
    http://www.idoubi.net/unreal/blueprints/2899.html
    Unity3D魔幻英雄角色编辑器 Fantasy Heroes- Character Editor [PRO]
    http://www.idoubi.net/unity3d/tool/3449.html
    Unity3D 用于Facebook的简单的排行榜 Easy Leaderboard (Facebook + PlayFab) v2.0.1
    http://www.idoubi.net/unity3d/tool/2525.html
    Unity3D Youtobe视频播放器 Youtube video player
    http://www.idoubi.net/unity3d/tool/2629.html
    Unity3D 完美的角色编辑器 Fantasy Heroes: Character Editor [PRO]
    http://www.idoubi.net/unity3d/tool/2668.html
    Unity3D 安卓视频播放插件 WRP Android Video Player Pro
    http://www.idoubi.net/unity3d/tool/2620.html
    Unity3D 网页图片链接插件 Web Image Links
    http://www.idoubi.net/unity3d/tool/2602.html
    Unity3D 简便的排行榜功能 Easy Leaderboard (Facebook + PlayFab) v2.0.1
    http://www.idoubi.net/unity3d/tool/2585.html
    Unity3D MMORPG 拍卖行系统 Auction House for uMMORPG
    http://www.idoubi.net/unity3d/tool/2553.html
    Unity3D PlayMaker NGUI脚本插件 PlayMaker NGUI Scripts
    http://www.idoubi.net/unity3d/tool/2352.html
    Unity3D 场景序列帧编辑器 Flux v2.1.5
    http://www.idoubi.net/unity3d/tool/2366.html

    转载于:https://blog.51cto.com/14259258/2382341

    展开全文
  • Unreal Engine 4 动画系统介绍

    千次阅读 2018-05-10 10:39:45
    Unreal Open Day 2017 活动上 ,Epic Games 资深开发者支持工程师王祢先生为到场的开发者介绍了在 Unreal Engine 4 中动画系统,以下是演讲实录。 大家好!鉴于引擎移动端功能以及 UI 优化都有同事做了介绍,今天我...

    Unreal Open Day 2017 活动上 ,Epic Games 资深开发者支持工程师王祢先生为到场的开发者介绍了在 Unreal Engine 4 中动画系统,以下是演讲实录。 

    大家好!鉴于引擎移动端功能以及 UI 优化都有同事做了介绍,今天我选择讲的主题是关于动画。动画是一个非常复杂的系统,我会主要介绍一些基本的概念,大家在了解了基本概念后,就可以在上面做出扩展。我并不会教大家怎么使用动画工具,关于一些动画节点的使用,我们的在线文档上都有比较详细的说明,也有比较多的资源。今天不会讲到的内容包括 Morph target,IK,Retargeting,Rootmotion,Additive,Skeletal Control 这些。

    首先,我们先来看看引擎中的动画系统是如何工作的。为什么我要先讲解这样一个问题,因为国内有很多用户在使用动画系统的时候,有很多疑问。这些疑问并不是因为他们没有查阅文档,而是因为没有理解系统的工作方式。本质上,动画系统工作原理是非常简单的,我这里还是重新介绍一下。


    我们先来看看在引擎中动画相关的资源主要分为哪几类。

    第一大类是最基本的数据资源。其中主要来自于外部 DCC 工具制作并导入的原始资源,我们称之为 Anim sequence。

    然后,有些资源可能为了制作和导入的方便是分散开来的,但是有些情况下会组合到一起使用。所以引擎中有一种资源叫 Anim Composite。他是使用多个 Anim sequence 或是自身(Anim Composite)所组合成的资源。在使用时,依然被看作是普通的 Anim Sequence。

    第三种数据资源类型叫 Blendspace。他可以是一维的也可以是二维的。二维的情况下,在两个轴上,通过变量控制对任意在二维平面上指定的动画序列(Anim sequence)作混合。对于任意的二维输入,总能找到这个输入值在二维图像附近最接近的四个动画序列按照权重来混合。严格来说,Blendspace 并不是单纯的基础数据,他也受其它输入参数的影响来混合 Pose 。但是,由于在动画混合蓝图中是作为 Pose 的输入结点,我们这里依然把它作为数据类资源。

    第四种数据资源叫 Montage。这一类资源一般是直接受逻辑控制的组合资源。


    在数据资源的基础上,我们还可以绑定一些额外的数据。

    第一类常用的数据类型叫 Notify。引擎包括一些内建的 Notify类型。譬如,在走路的时候希望脚步踩到地面的那一刻,触发踩地面的事件,用来向地面投射贴花,用于产生脚印,以及播放脚步音效或扬起尘土的特效之类。这里的 Notify 你还可以扩展成你自定义的事件类型,可以在蓝图以及代码中去处理事件对应的逻辑。举个例子:如果做一个动作或格斗类游戏,在出招的时候,判定并不是从这个动画开始播放的时刻就已经有了的,可能是从出招动画到某一时刻开始,才有打击判定。那么我们就可以通过 Notify 来用事件通知游戏逻辑在特定的时候去打开和关闭判定。

    第二类叫 Curve。Curve 就是伴随动画序列的时间轴所绑定的曲线数据,后面会有一些举例。再然后你也可以绑定一些你自定义的数据类型。


    讲完刚刚这些数据类型,接下来就是最重要的处理动画混合逻辑的资源,叫 Anim instance。Anim sequence 的设计是基于对于 3A 级游戏中复杂的动画需求所产生。这里有一个假设,那就是动画状态在复杂的情况下一定是需要对骨骼结构有认知的。所以引擎中的 Anim instance 和骨骼是强耦合的关系。譬如你需要知道腰部的骨骼位置来区别开上半身和下半身的动画,这样的设计可以完成相当复杂的动画混合,但是却也带来了一些限制。如果我的整个动画状态只需要简单的一个状态机在不同的状态中,譬如闲置、追逐、攻击、受击、死亡,在每中状态中,并不作复杂的混合,而只是播放一个简单的 Anim instance。在整个逻辑中完全不需要用到骨骼信息。那么照理来说,即使拥有不同骨骼结构的对象,如果只需要这个简单逻辑的话都可以共享这套逻辑。然而由于我刚刚所说的 Anim instance 和骨骼的强耦合设计导致在现在的引擎框架下,这样的功能暂时无法完成。我们在内部也在作一些讨论,以后可能会有支持纯逻辑的 Anim instance 功能,而目前来看,如果大家有这样的需求,我建议在可能的情况下把这些对象的骨骼层次结构尽可能保持一致,这并不是说多个对象的骷髅要完全一致,而只是骨骼树的层次结构一致就可以了。譬如你的基础骨骼是个人形,有些怪物会多出尾巴或翅膀,这些多出的骨骼并不破坏原先的树状结构,而只是多出来的分支。所以还是可以利用 Retargeting 来共享 Anim instance 的逻辑。


    Anim instance 中,最明显的两块分别是 EventGraph 和 AnimGraph。其中 EventGraph 就类似于普通的蓝图,用来在 tick 的时候处理一些逻辑状态的更新以及播放 Montage。当然这些逻辑也可以在 C++里面做。AnimGraph 是用来混合和输出 Pose 的地方。说到混合,我们可以把每一帧中整个混合的过程看成是一棵树,从叶子结点输出的 Pose 经过枝干结点的混合计算输出到根结点的最终 Pose。我们刚刚说到的数据类的资源,就是这里所谓的叶子结点。这些结点本身不需要其它的 Pose 作为输入,而直接提供了 Pose 的输出。而枝干结点则是进行混合的结点,当然真的说混合也不是很准确,有些枝干结点只需要输入一个 Pose,在自己的结点逻辑中,对这个 Pose 作一些修正,并不进行混合。我们把这些枝干结点计算调整和混合Pose 的行为称作评估(evaluate)。举个最简单的枝干结点的例子,那就是多结点混合。譬如,输入的有两个 Pose ,一个权重是 0.8,另一个是 0.2,相当于是把第一个 Pose 的 BoneMap 的 transform 乘以 0.8,第二个乘以 0.2,再相加输出。这里我列了一个树状图,来表示动画混合的过程。但是因为这是个非常简化了的例子,所以其中不包括直接对骨骼进行控制或者直接 Override 一个 Fullbody slot 来强制更新整个 BoneMap 之类的行为。并且一般来说,一个正常的 anim graph 的一帧的混合也不会像这张简化图这样是棵红黑树。首先,就像我刚刚说的,你并不能保证他是二叉的,譬如刚刚说的多混合结点完全可以由三个或以上结点来混合,以及我刚刚说的有些枝干结点,只有一个输入。再者,大部分情况下他也不会是平衡的。在混合状态复杂的情况下,我们一般会分层次来混合,这就导致了这棵混合树会往一个分支方向衍生出去。



    好了,那么刚刚看到的是单帧的 Pose 混合计算情况。当持续到多帧以后,情况又会稍微复杂一些。譬如说两个 Pose 混合起来,他们的长度很有可能不一样。举例,我有一个走路的动画,他可能长达 2 秒,同时我又有一个跑步的动画,他长达 1 秒。如果我直接混合,就会出现很怪异的情况,譬如走路还在迈左腿的时候,跑步已经迈右腿了,混合起来的姿势就会非常奇怪。基于这种情况,我们引入了 Sync Groups 的概念,当我们设置这两个动画序列在同一个 Sync Groups 下进行混合时,引擎会把当前混合时权重较高的作为领导,把剩下的序列缩放到和领导序列一样长的情况,再按比例去做混合。这样就能解决动画长度不一致的混合问题。



    再来看多帧动画状态下,如果状态复杂,动画树上的某些分支在不同的帧内是完全不同的状态。为了简化树的逻辑,动画混合系统中可以使用状态机来隔离每一帧的状态。我这里的图例举了一个比较简单的 Locomotion 的状态机。

    关于动画混合的这棵树,在复杂的情况下,我们还会把他做分层。也就是把一棵混合完的树的根结点缓存下来,作为另一棵树的叶子结点。当然你也可以把整个复杂的树连到一起,分层只是为了便于维护和调整。这个图片是我们的 MOBA 游戏《虚幻争霸》中一个角色分层混合的模版示例。


    讲完了动画的基础概念后,我们来看一些例子加深理解。

    子树类用例。在引擎中有一类功能叫 Sub anim instance。这就类似于刚刚说到分层里面的一棵子树,这个子树可以拥有一个输入结点,并且输出一个 Pose 。典型的应用方式,是把在同一个逻辑下有多种可替换的子逻辑分离开,做到不同的 Sub anim instance 中。这样可以把剩余的逻辑用来共享。通过替换不同的 Sub anim instance 来组合出最终不同的效果。



    接下来讲一些叶子类的用例。通常的叶子类结点就是我们刚刚说的数据类结点,我这里举两个比较特殊的例子。在 4.17 版本中,我们会加入一个叫 live link 的结点。它通过引擎的消息总线从外部实时读入数据输出Pose 。这里的输入源可以是各种 DCC 工具,也可以是动作捕捉或手势识别类设备。在我们放出的第一个版本中,会带有一个 maya 的实现,通过 maya 的插件把在 maya 中当前动画的 BoneMap 数据通过 live link 消息总线和引擎进行通信。引擎把接收到的数据转换成引擎内的数据输出当前的 Pose 。这样就可以做到在 maya 中一边播动画一边在引擎中看到效果了。



    下一个叶子类结点的举例,叫 Pose Snapshot。Pose Snapshot 就是把任意指定帧的 BoneMap 记录下来,在接下来的任意时刻,用来作为数据源输入和其它 Pose 做混合。譬如在 Robo Recall 中,你打倒了机器人,机器人会进入物理状态而倒地。你可以把这个状态存下来,在之后再和站起来的动画作混合。


    刚刚举了两个叶子类结点的例子,我们再来看看动画混合中最大的一类——枝干类结点的例子。大部分情况都是多个 Pose 按权重进行混合,当然也可以是按照 bool、int、enum 值进行混合。我这里依然举一些特殊的例子。


    第一个例子是 RigidBody 结点。在讲这个结点前,我要先介绍一个伴随而来的概念,叫 immediate mode physics。引擎中以前的 Physics 是所有的 RigidBody 都加到同一个 PhysX scene,这种情况下如果每个角色身上都有多个需要计算物理的 RigidBody,场景中又有大量的这样的角色,计算量就相当的大。但是大部分时候角色互相之间的物理碰撞细节大家并不关心,所以这样的效率比较低。

    因此我们和 Nvidia 进行了合作,他们对 PhysX 的 Api 进行了调整。在新版本中放出了更底层的 Api 可以让我们在引擎中做更细致的控制。大家可以看到这个新的 immediate physics,一个角色身上所有的 RigidBody 都只注册在当前这个 skeletal mesh component 下,多个 SMC(skeletal mesh component 缩写)之间并不会有交互,这样很大程度上提高了运行的效率。

    大家可以看到,这里的视频同屏有几百个小兵站在地上做闲置的动画,在受到物理冲击后转入到物理状态。这么大量的物理对象在我的笔记本上依然能稳定在 60 帧,而右边的图也显示了单个较为复杂的角色在模拟物理时候的开销,只使用了 0.24ms。大家可能觉得这是一个纯粹物理的功能,为什么我放到动画的枝干结点的例子里来讲呢? 因为事实上你可以在动画中把动画计算完的 Pose 输入进去,在这个结点中根据当前动画的Pose 和前一帧计算完的结果计算出骨骼结点的变化,从而模拟出物理受力的变化,并根据输入的权重混合回你的 Pose 。有了这样的功能,做我之前说的 Robo Recall 中很自然的击倒机器人或者拳击类的游戏、以及用枪射击怪物时怪物比较自然的受击都变得相当简单。



    好了,下面我们再来看另一个枝干结点的例子。我们称之为 Speed Warping。传统的游戏中如果你调整了移动速度,那么为了不产生滑步你也需要调整跑步的动画播放的速率。譬如你的速度翻了一倍,那么很多时候你就需要把动画也加快一倍播放,大家可以看到在这里的视频右边加快播放后的动画其实是很别扭的。真实情况下我们提高速度除了迈出的脚步速度会有一些变快以外,更多的情况下,其实是调快了步幅。同样的减慢速度也是这样。所以 Speed Warping 就是做了这么一个效果。那我们是怎么计算的呢?



    简单来讲,原始的动画双脚的位置是这里的红球。我们计算他跟腰部垂线的水平距离并根据加减速的倍率横向扩展。譬如当是 2 倍的时候,调整到绿球的位置。但这个时候两只脚的距离被拉的太长了,因此我们适当的往下调整了屁股的位置,并且将两只脚以刚才绿球所在位置到屁股的连线上挪动一段距离使得脚步的长度保持不变,所以最终计算出来的就是蓝球的位置。



    我再举一些其它的例子。比如引擎中当你对 AnimBP 进行继承的时候,所创建出来的内容叫 Child AnimBP ——它所做的事情是让你重载所有的叶子类结点。举个实用的例子:譬如我有一种敌人,他永远是从初始的出现状态到发现玩家到向玩家攻击这样转化,而这样的怪物在地图上不同的场景下有不同的出现动画,有可能是从地上爬出来的,有可能是从墙上跳出来的。对于这个怪物来说,他的动画切换状态都是一样的,所不同的只是初始状态所需要使用的资源,所以只需要替换初始的动画(某个叶子结点)就可以了。



    再举一个例子,有不少人问过,在《虚幻争霸》中,是怎么做到让角色不滑步的。传统的主机游戏中,为了让脚不滑步很多时候我们都是使用 root motion 来做移动的动画。但是因为《虚幻争霸》是个 MOBA 游戏,策划会希望能够用数据来驱动移动的速度。譬如在有不同的 buff 或者装备的情况下,角色的速度也会发生变化,这用 root motion 就很不好处理。所以我们做了一个叫 Distance Curve 的功能,这也是我刚刚说到的 Curve 数据的一种运用方式。我们可以把 Distance Curve 的方式看成是反向的 root motion。它通过给所有的启动、旋转、站定动画都加入曲线数据,曲线上的数值表示当前这帧动画到达站定点的位置的距离,其中站定点(Marker)是很容易预测的。


    当玩家的输入发生变化,引擎的计算在那一刻就能完成,可预测出最终速度衰减后站定的位置。通过查询曲线中动画到站定的距离可以直接从对应距离的那一帧动画开始混合。当然这些计算都有一些前提,首先,曲线中的数值在靠近站定点的动画中取负值,而远离取正值。这种时候 Piviting 行为也就是你在往左走的时候突然往右,这条曲线是从负值到正值的,这样这条曲线就满足了无论什么情况下都单调递增并且除了0其它的值都不会重复,这就方便我们在O(n)复杂度下找到对应的动画帧数。



    举完了这些例子以后我们来看看动画的优化。优化是个很大的话题,有很多方面。有些是可以在设计上规避掉的,有些是则是在内容上做了优化。虽然今天我不对这些做举例,但其实引擎也有工具可以直接在骨骼结构上右键设置在某一级 LOD 以下不更新这些骨骼,这也算是内容上的优化。那么接下来我主要讲在不希望太大的妥协效果的情况下,两大类优化的手段:

    其中一类就是降低人们低感知部分的采样频率。譬如空间上的 LOD 或者时间上的更新频率 URO,基本思路就是离的远的、占据视频面积小的、或者甚至是看不见的,降低更新的细节层次以及降低更新的频率。另外一类是尽可能提高利用硬件的计算能力,尽可能降低不同动画任务的依赖性来提高并行计算。



    在引擎中,SkeletalMeshComponent 中有一个 Update Flag 选项,默认是 Always Tick Pose。这意味着当 SMC 不被渲染到的时候,动画逻辑还是会 Tick,并且 AnimGraph 里的节点虽然不会计算 BoneMap 也就是没有实际的 Evaluate 计算,不过还是会计算对应节点的 Update,也就是计算这些节点的输入权重之类的数值。这使得在动画对象重新进入视野中进行绘制时,可以很自然的直接更新到最新状态下的姿态。所以大部分时候,不是不得已,都不需要使用 Always Tick Pose and Referesh Bones。而如果你对于一些不太重要的动画对象,甚至不关心他们不被渲染的时候 Pose 逻辑需不需要更新的情况下,可以进一步的选择 Only Tick Pose when Rendered 来进一步减小 CPU 的开销。



    另一个比较重要的设置是 AnimGraph 的各种枝干接点上的 LOD Threshold 选项,大部分这类需要进行 Evaluate Pose 计算的接点上,都会有这个选项,默认数值是 -1,也就是不会起效,如果设定了正整数值的话,就相当于在对应的 LOD 情况下,这个节点以及往下的子树就都不会评估了。对于同屏有大量骨骼动画对象的情况下,仔细调整和设置 AnimGraph 中各个节点的 LOD Threshold 能很有效的降低动画 CPU 的开销。



    再有一个是刚才说道的 RigidBody 接点的 LOD 优化,引擎在创建 RigidBody 加到当前 SCM 中的时候,已经根据所有的 LOD 从最下级到最上级进行了排序,这样一来,切换 LOD 后,自然而然的只要取列表的前几项做计算就可以了。



    再来我们说一下 URO,也就是更新频率的优化。例如,可以根据离 Camera 的距离,调整 Tick 的频率。我这里给了个开启 URO 的例子,甚至我们可以根据不同的 LOD 设置不同的频率,引擎中也有 LODMapForURO 的设置。



    那么我们再来看看怎么提高并行。由于 BP 是在虚拟机上执行的,所以都是在游戏线程进行的,无法进行并发,所以如果有大量的动画对象希望提高并行的话,建议大家不要使用 AnimInstance 的 EventGraph 更新逻辑,而是写到 C++ 中,在自己的 AnimInstance 类中指定自己的 Proxy 继承类,并写到 Proxy 的 UpdateAnimation 中,这样引擎就能把动画的 Update 以及 Evaluate 都放到 Proxy 上通过其他工作线程并行执行。



    大家可能会注意到,在 ACharacter::PostInitializeComponents() 中,对我们的 MeshComponent 的 PrimaryComponentTick 加了一个前提条件,也就是角色的 CharacterMovement 这个 component 的 tick。因为引擎希望当前这帧的动画更新的信息是基于移动后的位置进行的。如果大家不需要这样准确的依赖,还可以在自己的角色继承类中,重新去除依赖,来使得动画的计算能更早的利用工作线程并行计算。



    再有一点,基于 UE4 的网络模型,服务器端在默认情况下也会有不小的动画计算开销,我们其实可以在大部分时候做一些优化,譬如关闭服务器的物理状态计算。如果不需要很精确的在服务器端计算角色的动画变化,可以保证服务端的计算不依赖于骨骼位置,那么可以在服务端完全不评估整个动画(仅使用 Capsule 作角色位置的验证或计算)。如果还能保证所有的动画中触发的事件不会影响到 Gameplay 而只会影响表现,那么还可以关闭整个动画的更新和 tick。一般来说 Montage 是游戏逻辑直接控制的动画状态,那么我们可以把在这些动画中影响游戏逻辑的事件全都加在 Montage 上,所以只有在播放 Montage 的时候才需要 tick。最后,即使进行 tick 也依然可以使用之前说到的 URO 以比较低的频率来 tick。




    这里是初始化的时候设置当进行播放和停止 Montage 的时候进行回调的例子:通过判断 AnimInstance->IsAnyMontagePlaying() 来决定是不是要允许 tick,这里的事例实现了根据当前是否在播放 Montage 在服务端自动调整是不是要 tick。因为服务器端从来不需要渲染,所以当客户端设置成了 OnlyTickPoseWhenRendered 的时候,服务端就可以完全不需要 tick。

    以上就是今天要讲的所有内容,谢谢大家。


    本文章转自http://gad.qq.com/article/detail/27926#

    展开全文
  • 目录 一、概述 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

    展开全文
  • 入门VR游戏开发者在Unreal Engine 4上开始VR设计所需要知道的 游戏开发者 VR2048(vr2048) · 2016-01-12 18:38 首先,不要被标题吓到,这其实是一篇面向游戏开发者的入门科普文章,对于有致于开发...
  • 虚幻引擎3(Unreal Engine 3)概要

    千次阅读 2009-03-11 18:51:00
    虚幻引擎3(Unreal Engine 3)概要虚幻引擎3概要 虚幻引擎3是一个面向下一代游戏机和DirectX 9个人电脑的完整的游戏开发平台,提供了游戏开发者需要的大量的核心技术、数据生成工具和基础支持。 虚幻引擎3的设计...
  • 一年一度的行业盛会“2018 Unreal Open Day虚幻引擎技术开放日”将在下周三于上海正式开幕,持续两天为虚幻开发者带来满满技术干货。本次大会将分为A、B、C三个会场同时进行分享,囊括主旨、技术、游戏、行业应用、...
  • 3D游戏引擎剖析

    千次阅读 2015-02-21 10:11:39
    介绍 自Doom游戏时代以来我们已经走了很远。 DOOM不只是一款伟大的游戏,它同时也开创了一种新的游戏编程模式: 游戏 "引擎"。 这种模块化,可伸缩和扩展的设计观念可以让游戏玩家和程序设计者深入到游戏核心,用...
  • 本文是阅读 IBC 2019上面五篇文章的阅读笔记。 一、Comparative Analysis of Real-time Multi-view Reconstruction of a Sign Language Interpreter 手语翻译员的多视角重构的对比分析 1. 背景动机 ...
  • 3D游戏引擎是个什么玩意 ... 泡泡网显卡频道5月23日 在某游戏中的一个场景中,玩家控制的角色躲藏在屋子里,敌人正在屋子外面搜索玩家。突然,玩家控制的是一个穿迷彩服的士兵,突然碰倒了桌子上的一个杯子...
  • 主流游戏引擎大全

    千次阅读 2012-09-23 16:44:59
    尽管在某些技术之间存在着竞争关系——Unreal Engine 3,id Tech 5,CryENGINE 2,或许还有Source,但是实质上自主研发的引擎才被看作是他们之间关注的焦点。目前,业界已逐渐接受了这样的解决方案,那就是巩固...
  • (转)主流商业游戏引擎一览

    千次阅读 2010-10-20 20:42:00
    尽管在某些技术之间存在着竞争关系——Unreal Engine 3,id Tech 5,CryENGINE 2,或许还有Source,但是实质上自主研发的引擎才被看作是他们之间关注的焦点。目前,业界已逐渐接受了这样的解决方案,那就是巩固...
  •   Training Deep Networks with Synthetic Data: Bridging the Reality Gap by Domain Randomization 这篇论文来自英伟达,核心思想是用合成数据训练卷积神经网络(CNNs)...研究人员为Unreal Engine 4创造了一...
  • 开放大世界 codelikeme

    2020-02-06 12:33:23
    开放大世界 codelikeme 视频地址: B站: https://www.bilibili.com/video/av62125947?p=317 油管: ... 视频内容 ...近战动作游戏制作流程 47-275 角色 IK 制作流程 116-134 武器系统(弓箭...
  • 20180509导出网站

    千次阅读 2018-05-09 13:37:44
    Bookmarks书签栏固定标签页资源... YouTubeunity3d插件免费下载 商业资源代购 团购 unity外包 Unity时空淘宝网 - 淘!我喜欢Unity - Scripting API: AudioClip3D模型/环境unity3d资源unity3d资源 - Unity3d资源基地...
  • 网站导出20190118

    千次阅读 2019-01-18 11:07:02
    Bookmarks   书签栏   重要网站 ...Artlist.io - Music ...unity3d插件免费下载 商业资源代购 团购 unity外包 Unity时空 淘宝网 - 淘!我喜欢 Unity - Scripting API: AudioClip 3D模型/环境 unity3...
  • 十大最牛游戏引擎

    万次阅读 2016-06-07 11:54:07
    从光影声效到场景细节,从画面触感到各种细腻体验,再到人物表情的捕捉,花草树木的美感等等。 毫无疑问,从业界各类大作真正进入3D时代开始,一款成功的游戏引擎,就从诸多方面展现出了其对作品整体质量不可估量的...
1 2 3 4 5 6
收藏数 116
精华内容 46
关键字:

unreal 动作捕捉插件