【Unite Tokyo 2018】Achieving high quality Anime style rendering on Unity

这里来介绍一下米哈游崩3的相关技术,技术分享于2018年,虽然过去5年了,但其中依然有些内容值得借鉴学习。

崩3、原神使用的都是Unity引擎,全文目录给出如下:

下面介绍了崩3在移动端上用到的一些特性,包括但不限于Bloom、动态粒子、Distortion(一种后处理)以及平面反射:

介绍一下反射实现逻辑:

  • 以1/3的分辨率进行渲染(不是PPR或SSR做的后处理),从下面的截图来看,这里是对反射渲染的viewport做了缩减处理的
  • 根据需要进行模糊处理
  • 渲染的时候考虑了菲涅尔效应
  • 通过材质处理,在金属反射面上增加了一些扭曲与sketch(笔触)效果

屏幕空间的扭曲自然是通过后处理完成:

  • 会基于深度来控制扭曲强度

Bloom逻辑没什么新意,还是多层下采样叠加后得到Bloom效果,并将之叠加到原始场景上去。

最后混合就得到了相关效果,中规中矩。

崩3有日夜变化与动态天气。

云层是通过贴片来实现的,不过提供了较为丰富的配置以应对不同的场景:

  • 时间(早中晚)
  • 颜色(Bright/Dark/Rim/Second Dark Color)
  • 将贴图分成多个Layer,用不同的通道表示,在实际使用的时候根据配置对Layer进行叠色混合得到最终效果

为云层提供了专门编辑器:

  • 通过billboard粒子实现
  • 可以自定义自己想要的云朵类型
  • 通过keyframe实现云层的动态变化
  • 添加了TOD时间控制逻辑

上面展示了可以实现的效果,看起来还挺丰富的,也挺好看,就是不知道运动起来是否连贯,而且贴图消耗低(内存稍高),性能比较好,如果没有角色穿云的玩法,这种方案还是挺不错的。

天气系统做了如下工作:

  • 对大气雾效的控制逻辑做了优化
  • 添加了对skybox颜色配置的逻辑
  • 为角色添加Lighting Volume

又绕回后处理了,增加了多层DOF效果:

  • 采用六边形作为DOF高光形状
  • 基于模糊半径(通过曲线控制,可以实现一些镜头效果或者随着时间动态变化的效果)对分辨率进行调控(半径越大,分辨率越低)
    支持对Bokeh的强度(会根据场景的亮度动态调整,以确保效果)与旋转参数进行调整以实现动画效果,下面给了一个图片,文末的参考链接中有对应的视频可以看下:

雨效逻辑:

  • 会根据时间的流速(如慢动作)调整表现
  • 一共设计了四种流速,并关联了四种不同的资产

下面看下卡渲逻辑。

角色着色要点:

  • 基于多层Ramp图+Brush实现
  • 头发做了各向异性,还做了折射和模糊?
  • 主要有三种光:方向主光、环境光IBL以及边缘光
  • 阴影用了PCSS(根据caster到receiver的距离调整阴影的软硬程度)
  • 描边

崩3对Ramp图的使用可以说是一大亮点,这里来介绍一下具体的实现逻辑。

早前的卡渲采用的Shading逻辑都是二值化处理,即基于NdotL的数值来判断当前像素是处于亮部还是暗部,这样导致的效果就是明暗交界过渡比较生硬,为了解决这个问题,就开始围绕NdotL的取值做文章,因为这个数值本身是个浮点数,因此我们可以将二值化处理变成分段的多值化处理,还可以更进一步,基于这个数值来对一张颜色贴图(称之为ramp图)进行采样,得到更为柔和的效果,就如上图所示。

关于ramp图,其实有很多不同的用法,下面做一下简单的整理:

  1. 一维Ramp图

比如有人基于这个数值来对shading的暗部判定逻辑进行调整从而得到更符合需要的明暗变化:

float NdotL = dot(L, N)*0.5 + 0.5 // half-lambert
float ShadowLerp = SAMPLE_TEXTURE2D(RampTexture, 1 - NdotL)
float3 Diffuse = lerp(BaseColor, ShadowColor, ShadowLerp)

又比如基于采样ramp图来对Shading Color进行调节:

float NdotL = dot(L, N)*0.5 + 0.5
float ShadowLerp = SAMPLE_TEXTURE2D(RampTexture, 1 - NdotL)
float3 Diffuse = BaseColor * ShadowLerp
  1. 二维Ramp图
    根据二维Ramp图采样的UV坐标含义不同,我们有不同的用法

2.1 TOD+MaterialID

上面这张Ramp图可以分成上下两半,上面的暖色调(三行颜色)对应的是白天的参数,下面的冷色调(三行颜色)对应的是晚上的参数。而水平坐标对应的实际上材质ID,基于不同的材质ID,我们可以得到不同的颜色,从而可以为不同的物件指定不同的颜色叠加效果。

2.2 LightingAngle+Distance

也有人将横坐标解释成NdotL,纵坐标解释成到相机的距离的,从而可以实现颜色随着远近而变化的效果

2.3 LightingAngle+MaterialID
这个方案就是崩坏所使用的方案,横坐标跟上面一样,为NoL,纵坐标则为材质ID,从而使得不同的材质可以有不同的颜色变化效果。

不过这里其实并不是直接简单采样就拿来使用,还做了比较多细节处理:

  • 一张Ramp图会分成多个通道,每个通道用于控制一个layer的叠色
  • 而每个通道的数值其实可以理解成每个layer颜色的权重,我们可以指定每个通道对应的颜色,以及对应权重的偏移值:

整体的计算公式给出如下:

float3 Tint = SAMPLE_TEXTURE2D(RampTexture, float2(NdotL, MaterialID))

float3 TintHigh = Tint.r * TintHighColor
float3 TintMed = (Tint.g + TintMedOffset) * TintMedColor
float3 TintLow = (Tint.b + TintLowOffset) * TintLowColor

float3 TintColor = saturate(TintBaseColor + TintHigh + TintMed + TintLow)

这里给出了不同叠加层数下的不同效果。

通过对ramp图的调整,我们可以实现软硬切换的效果,且必要的时候,还可以为同一个角色的不同部位赋予不同的软硬程度。

明暗的渐变效果看起来不错,但也不是所有部位都需要,比如脸部就不要这个效果,因此这里还增加了一张mask图用于控制需要进行渐变的区域

这里的阴影看起来不是基于光源视角的,而是基于相机视角(或者说基于相机视角做一个调整)产生阴影贴图,之后根据偏移+阴影贴图采样来实现的效果,且透明物体也会参与到阴影贴图的绘制中去。

眼睛增加了折射效果,折射的原理就是将打在球面(眼球)上的射线按照折射原理做一个偏转,并基于偏转后的方向完成贴图采样。

因为表面并不是平整的,按照斯涅耳(snell)定律来求取折射向量会比较复杂,《RTR》中给出了一个拟合公式:
RefractVector = (w - k)*Normal-n*View \\ n = \frac{n1}{n2} \\ w = n * dot(View, Normal) \\ k = \sqrt{1+w^2-n^2}

具体的推导过程可以参考Faster Refraction Formula, and Transmission Color Filtering

有了折射向量,就能够算出偏移值:

如上图所示,入射点处的eye forward direction我们标注为Up,Up跟折射向量Refract所夹的锐角我们记为theta,Up与不做折射时的向量Direct所夹的锐角,记为alpha,那么我们就有:
cos\theta = abs(dot(\vec{Refract}, \vec{Up})) \\ cos\alpha = abs(dot(\vec{View}, \vec{Up})) \\ \vec{HorizontalOffset} = normalize( \vec{Up}*cos\theta + \vec{Refract}) * Height*(tan\alpha -tan\theta)

这里的height是入射点到水平面的高度,得到上述偏移向量后,将之与(眼球)贴图的U方向的向量(世界空间)以及V方向的向量(世界空间)点乘就得到了UV偏移值(上图公式给的应该是简化版本)。

眼睛绘制的另一项重要特征是焦散(Caustic),人眼结构如下图所示:

由于前房区域可以看成是一个透镜,而透镜会有聚焦光线的作用,因而导致虹膜部分会形成焦散的亮斑,如下图所示:

回到崩3这边的实现,由于卡渲并不追求物理真实,只要好看就可以了,因此完全可以把焦散绘制到贴图上,崩3采用的也是类似做法:

  • 添加一张Caustic Mask图,用于控制哪些区域会存在焦散亮斑
  • 由于卡渲的特殊,米哈游希望焦散效果出现在入射光线的另一侧,所以这里通过对diffuse计算逻辑进行翻转(将入射光线沿着法线做对称翻转)来模拟光照的变化,之后辅助菲涅尔公式(光在进入不同介质时会存在反射与折射,这个公式揭露了反射光强与折射光强的比例)来调节焦散强度的变化

眼睛的最终渲染效果如图所示,下面来看下头发渲染。

这里先列几个期望达成的渲染要点:

  1. 要能做到所见即所得的效果调节,即对参数或者贴图的调整,要能立马看到效果
  2. 要支持基于切线的各项异性
  3. 支持多层高光效果
  4. 支持随着粗糙度而变化的光照表现,且能够支持随着视角或光照的变化而跟随流动的效果与头发之间的AO遮挡

头发渲染的一个重要特征是高光,最常用的模型是Kajiya-Kay(简称kk),这个模型是目前渲染头发比较常用的,《神海》以及这里的崩坏都用的这个模型。

相对于普通物件的高光使用的NoH,这里使用的是ToH,但实际推导起来,两者其实是对等的:
sin(T, H) = sin(90^\circ- \theta) = cos(\theta) = cos(H,N)

kk高光项计算规则给出如下:

可以看到,这个公式跟上面崩坏使用的代码是吻合的

前面说到,崩坏这里用了两层高光,分别是低频跟高频,不确定是否每一层都用的kk(相当于采用的是Marschner模型了),为了模拟动漫中类似于电磁波抖动的效果,这里对高光做了抖动:

shift = Sample(NoiseTex, shiftUV)
shiftedT = T + shift * N
newTangent = normalize(shiftedT)

首先对采样坐标进行处理,得到噪声贴图的采样坐标,基于这个坐标得到抖动的幅度,基于抖动幅度利用法线对切线进行调整,从而实现高光在发束上下移动的效果。

这里给出两层高光的效果。

这里给出另一种高光实现方案,这里的高光想要实现顺着发丝移动的效果,且形状能够随着位置的不同而变化。

要想实现这种效果:

  • 需要确保每一缕头发的uv是顺着发丝而平滑变化的(方便基于uv计算出切线,否则就需要用flowmap来获取切线)
  • 对于每一缕发束,需要通过某个贴图标识出其左边界与右边界,从而可以实现从左到右高光形状的变化,并用多个曲线来定义高光模板实现不同的高光效果
  • 之后,同样使用一张噪声贴图来控制抖动(高光粗细)
  • 在材质上,添加若干参数用于控制高光的形状、偏移等,从而实现各种不同的高光形状

另一个需要各向异性高光的材质是丝绸,崩3这边的做法是利用副切线来进行计算,并使用了三层高光叠加的方式来实现,每种高光都可以分配不同的颜色。

这里没有具体介绍高光的计算方法,也没有给出三层高光的计算区别,不过移动端角色渲染(丝绸篇)对PBR下的写实高光计算逻辑进行了梳理,大概思路给出如下:

  1. 获取某个点的各项异性数值anisotropy,并基于这个数值计算出在切线T与副切线B方向上的粗糙度:
anisoAspect = sqrt(1 - 0.9 * anisotropy)
roughnessT = roughness / anisoAspect 
roughnessB = roughness * anisoAspect 
  1. 基于T、B以及对应的roughness计算G项跟D项,F项不受各项异性影响
  2. 基于计算得到的GDF,计算对应的高光
  3. 为了得到更逼真的高光效果,通常需要对法线做偏移调整

说回到崩3的3层高光,推测可以在两个层面做文章:

  1. 为不同层指定不同的anisotropy或roughness
  2. 为不同层指定不同的法线偏移参数

这里是演示效果,看起来还挺赞的

这里介绍一些特殊的装饰材质,如水晶等需要折射效果的透明材质,以及纱巾等需要模糊的半透材质,具体实现逻辑没什么好介绍的,其中一个可以说一下的优化点是这里的折射与模糊不是针对全屏的,而是针对角色(绘制一个proxy mesh,这个mehs通常是对应的角色,或者更精简一点,只绘制半透物件对应区域)覆盖的区域的

下面来看看描边算法。描边使用的是基于法线外扩的方案,只针对角色以及动态物体使用,并且通过顶点属性来控制描边的粗细,从而可以实现发梢描边逐渐变细最终缩小为0的效果,且描边粗细还会随着到镜头的距离做动态调整。

此外,不同的材质,其描边颜色也是可以控制的(这里没有介绍是基于顶点属性还是贴图来实现)

对于几何边缘而言,法线外扩方案就够了,但是对于一些非几何边缘,如一些色彩边缘来说,这个方案就无能为力了,崩3的做法是通过一个预处理(没有介绍预处理是在什么时候完成,是一次性的,还是每帧都需要,从理论上推测,可以做成一次性的,在开发阶段就制作完成,比如将之保存成一个额外的mesh,之后通过geometry shader完成对应的线条勾勒)来搜集这些边缘的数据,并且基于与法线外扩方案同一套控制参数(顶点属性控制粗细等逻辑,以及描边颜色控制逻辑等)来对效果进行调整,确保全屏效果一致。

场景的描边,通常是基于normal与depth的边缘检测后处理实现的,但是这种方案的问题在于描边的宽度不好控制,虽然能够基于深度来控制,但是其调整范围是有限的,只能实现少数几个像素尺寸的调整(为什么?),所以不适合角色等距离相机较近的人物(较近的话,就只能考虑基于法线外扩来实现了)。

除了普通的描边之外,这里还介绍了一种比较复杂的笔刷描边方案,其实现步骤给出如下:

  1. 轮廓线提取:从Mesh上提取轮廓边,可以分为Sharp Edge和Smooth Edge两种
  2. 连接轮廓线:根据模型的拓补关系,将相邻的轮廓边连接成尽可能长的轮廓线(是不是离线计算会比较合适?)
  3. 轮廓线分段:在步骤2的基础上,根据轮廓线上曲率和可见性的变化,将轮廓线在曲率或可见性的突变处分开
  4. 笔触映射:将想要添加的笔触(某种人工绘画时会呈现的一种特定的描边效果)制作成纹理贴图,根据对应的纹理坐标映射到步骤3的轮廓线上

这种方案实现的描边效果更为风格化,但是性能消耗较高,不太适合用在实时,离线的CG制作倒是可以。

下面来看下崩3中的特效或后处理效果。

这里展示了体积光+bloom的效果,可以说是非常的抓人眼球了。

来看下体积光是如何实现的:

  1. 看起来是用的Unity自带的功能,通过曲线来控制体积光的形状
  2. 可以通过曲线在运行时调整体积光的形状与强度(颜色)
  3. 增加了一张3D噪声贴图模拟雾气动态变化的效果
  4. 增加了Cookie Map用于控制体积光投影的形状(这个没有细说,不太理解,不过不重要)
  5. 通过蓝噪声抖动+TAA来降低体积光采样频率过低导致的锯齿问题

开了GI,支持基于视频的动态自发光与体积聚光

为角色添加了动态AO,基于魔改后的HBAO实现

这里是用到的一些屏幕空间后处理效果,部分如Lens Flare需要为卡渲风格做特定调整(这里使用与bloom类似的方式提取的高光区域作为输入,然后进行多次不同方向上的卷积并应用色彩调制来获得最终结果)

这里展示的是CG画质的渲染效果,看起来还是挺不错的

为了得到高画质的场景效果,崩坏3采用的策略是在PBR的基础上对其做风格化调整,使之表现接近动画风格:

  1. 对于色彩的做了卡通化调整
  2. 对于物体材质细节根据需要进行强调或省略
  3. 结合使用图像空间的勾线来强调物体边缘

这里是效果展示。

角色的表情是基于blendshape制作实现的:

  1. 将眼睛,嘴巴和眉毛拆分为单独的部件,分别控制其表情变化
  2. 自研了一套面部表情插件,用于实现表情动画的及语音嘴型的自动映射
  3. 预定义了一个包含丰富表情的集合,在角色交互的时候根据需要从中选用以驱动面部表情。

这里介绍了角色动画实现上的一些细节:

在Unity中如果使用humanoid作为动画导入方式,当关节处旋转角度较大时,关节处的形状可能无法满足项目组对动画品质的期望, 解决方案是:
1.1 在DCC中为每关节添加一个修正的blendshape,并将之一并导入到Unity以防止关节形变
1.2 由一个脚本来根据关节旋转角度来对形状进行插值混合,为了确保效果,每个关节会需要分别制作两个blendshape,一个用于90度,另一个用于140度以对关节形变补偿

  1. 另一种解决方法是使用额外的骨骼进行关节修正,这种方法更容易制作,但是对于结构细节的优化效果不如上一种使用blendshape的方案

流体和破碎是使用alembic格式,或者EXR贴图作为载体从Houdini或其他DCC工具导入的顶点动画实现的。

基于贴图的顶点动画由于是在GPU上计算实现的,其执行效率及加载速度要快于alembic格式

实时卡通渲染在今后可以继续改进和完善的地方:

  1. 实现所有类型材质完全可定制的风格化渲染,目前崩3只在人物皮肤和服装渲染中的应用了笔刷以获得笔触效果,后续会考虑将其扩展到整个场景的渲染,比如实现类似新海诚式的场景风格,以呈现有着独特且统一的风格化动画风格渲染。
  2. 进一步提高模型的渲染精度,最终期望实时呈现CG级的模型精度,比如可以考虑使用geometry shader或预烘培displacement map进行动态自适应的曲面细分,相比直接导入原始高模,它可以极大减少资源导入的开销和提升运行效率
  3. 优化整套流程解决方案,使之更易于实时调整和编辑,进一步提升运行效率以适合在游戏中使用

参考

[1]. Achieving high quality Anime style rendering on Unity
[2]. 米哈游技术总监首次分享:移动端高品质卡通渲染的实现与优化方案

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容