这里来介绍一下米哈游崩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图,其实有很多不同的用法,下面做一下简单的整理:
- 一维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
- 二维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》中给出了一个拟合公式:
具体的推导过程可以参考Faster Refraction Formula, and Transmission Color Filtering
有了折射向量,就能够算出偏移值:
如上图所示,入射点处的eye forward direction我们标注为Up,Up跟折射向量Refract所夹的锐角我们记为theta,Up与不做折射时的向量Direct所夹的锐角,记为alpha,那么我们就有:
这里的height是入射点到水平面的高度,得到上述偏移向量后,将之与(眼球)贴图的U方向的向量(世界空间)以及V方向的向量(世界空间)点乘就得到了UV偏移值(上图公式给的应该是简化版本)。
眼睛绘制的另一项重要特征是焦散(Caustic),人眼结构如下图所示:
由于前房区域可以看成是一个透镜,而透镜会有聚焦光线的作用,因而导致虹膜部分会形成焦散的亮斑,如下图所示:
回到崩3这边的实现,由于卡渲并不追求物理真实,只要好看就可以了,因此完全可以把焦散绘制到贴图上,崩3采用的也是类似做法:
- 添加一张Caustic Mask图,用于控制哪些区域会存在焦散亮斑
- 由于卡渲的特殊,米哈游希望焦散效果出现在入射光线的另一侧,所以这里通过对diffuse计算逻辑进行翻转(将入射光线沿着法线做对称翻转)来模拟光照的变化,之后辅助菲涅尔公式(光在进入不同介质时会存在反射与折射,这个公式揭露了反射光强与折射光强的比例)来调节焦散强度的变化
眼睛的最终渲染效果如图所示,下面来看下头发渲染。
这里先列几个期望达成的渲染要点:
- 要能做到所见即所得的效果调节,即对参数或者贴图的调整,要能立马看到效果
- 要支持基于切线的各项异性
- 支持多层高光效果
- 支持随着粗糙度而变化的光照表现,且能够支持随着视角或光照的变化而跟随流动的效果与头发之间的AO遮挡
头发渲染的一个重要特征是高光,最常用的模型是Kajiya-Kay(简称kk),这个模型是目前渲染头发比较常用的,《神海》以及这里的崩坏都用的这个模型。
相对于普通物件的高光使用的NoH,这里使用的是ToH,但实际推导起来,两者其实是对等的:
kk高光项计算规则给出如下:
可以看到,这个公式跟上面崩坏使用的代码是吻合的
前面说到,崩坏这里用了两层高光,分别是低频跟高频,不确定是否每一层都用的kk(相当于采用的是Marschner模型了),为了模拟动漫中类似于电磁波抖动的效果,这里对高光做了抖动:
shift = Sample(NoiseTex, shiftUV)
shiftedT = T + shift * N
newTangent = normalize(shiftedT)
首先对采样坐标进行处理,得到噪声贴图的采样坐标,基于这个坐标得到抖动的幅度,基于抖动幅度利用法线对切线进行调整,从而实现高光在发束上下移动的效果。
这里给出两层高光的效果。
这里给出另一种高光实现方案,这里的高光想要实现顺着发丝移动的效果,且形状能够随着位置的不同而变化。
要想实现这种效果:
- 需要确保每一缕头发的uv是顺着发丝而平滑变化的(方便基于uv计算出切线,否则就需要用flowmap来获取切线)
- 对于每一缕发束,需要通过某个贴图标识出其左边界与右边界,从而可以实现从左到右高光形状的变化,并用多个曲线来定义高光模板实现不同的高光效果
- 之后,同样使用一张噪声贴图来控制抖动(高光粗细)
- 在材质上,添加若干参数用于控制高光的形状、偏移等,从而实现各种不同的高光形状
另一个需要各向异性高光的材质是丝绸,崩3这边的做法是利用副切线来进行计算,并使用了三层高光叠加的方式来实现,每种高光都可以分配不同的颜色。
这里没有具体介绍高光的计算方法,也没有给出三层高光的计算区别,不过移动端角色渲染(丝绸篇)对PBR下的写实高光计算逻辑进行了梳理,大概思路给出如下:
- 获取某个点的各项异性数值anisotropy,并基于这个数值计算出在切线T与副切线B方向上的粗糙度:
anisoAspect = sqrt(1 - 0.9 * anisotropy)
roughnessT = roughness / anisoAspect
roughnessB = roughness * anisoAspect
- 基于T、B以及对应的roughness计算G项跟D项,F项不受各项异性影响
- 基于计算得到的GDF,计算对应的高光
- 为了得到更逼真的高光效果,通常需要对法线做偏移调整
说回到崩3的3层高光,推测可以在两个层面做文章:
- 为不同层指定不同的anisotropy或roughness
- 为不同层指定不同的法线偏移参数
这里是演示效果,看起来还挺赞的
这里介绍一些特殊的装饰材质,如水晶等需要折射效果的透明材质,以及纱巾等需要模糊的半透材质,具体实现逻辑没什么好介绍的,其中一个可以说一下的优化点是这里的折射与模糊不是针对全屏的,而是针对角色(绘制一个proxy mesh,这个mehs通常是对应的角色,或者更精简一点,只绘制半透物件对应区域)覆盖的区域的
下面来看看描边算法。描边使用的是基于法线外扩的方案,只针对角色以及动态物体使用,并且通过顶点属性来控制描边的粗细,从而可以实现发梢描边逐渐变细最终缩小为0的效果,且描边粗细还会随着到镜头的距离做动态调整。
此外,不同的材质,其描边颜色也是可以控制的(这里没有介绍是基于顶点属性还是贴图来实现)
对于几何边缘而言,法线外扩方案就够了,但是对于一些非几何边缘,如一些色彩边缘来说,这个方案就无能为力了,崩3的做法是通过一个预处理(没有介绍预处理是在什么时候完成,是一次性的,还是每帧都需要,从理论上推测,可以做成一次性的,在开发阶段就制作完成,比如将之保存成一个额外的mesh,之后通过geometry shader完成对应的线条勾勒)来搜集这些边缘的数据,并且基于与法线外扩方案同一套控制参数(顶点属性控制粗细等逻辑,以及描边颜色控制逻辑等)来对效果进行调整,确保全屏效果一致。
场景的描边,通常是基于normal与depth的边缘检测后处理实现的,但是这种方案的问题在于描边的宽度不好控制,虽然能够基于深度来控制,但是其调整范围是有限的,只能实现少数几个像素尺寸的调整(为什么?),所以不适合角色等距离相机较近的人物(较近的话,就只能考虑基于法线外扩来实现了)。
除了普通的描边之外,这里还介绍了一种比较复杂的笔刷描边方案,其实现步骤给出如下:
- 轮廓线提取:从Mesh上提取轮廓边,可以分为Sharp Edge和Smooth Edge两种
- 连接轮廓线:根据模型的拓补关系,将相邻的轮廓边连接成尽可能长的轮廓线(是不是离线计算会比较合适?)
- 轮廓线分段:在步骤2的基础上,根据轮廓线上曲率和可见性的变化,将轮廓线在曲率或可见性的突变处分开
- 笔触映射:将想要添加的笔触(某种人工绘画时会呈现的一种特定的描边效果)制作成纹理贴图,根据对应的纹理坐标映射到步骤3的轮廓线上
这种方案实现的描边效果更为风格化,但是性能消耗较高,不太适合用在实时,离线的CG制作倒是可以。
下面来看下崩3中的特效或后处理效果。
这里展示了体积光+bloom的效果,可以说是非常的抓人眼球了。
来看下体积光是如何实现的:
- 看起来是用的Unity自带的功能,通过曲线来控制体积光的形状
- 可以通过曲线在运行时调整体积光的形状与强度(颜色)
- 增加了一张3D噪声贴图模拟雾气动态变化的效果
- 增加了Cookie Map用于控制体积光投影的形状(这个没有细说,不太理解,不过不重要)
- 通过蓝噪声抖动+TAA来降低体积光采样频率过低导致的锯齿问题
开了GI,支持基于视频的动态自发光与体积聚光
为角色添加了动态AO,基于魔改后的HBAO实现
这里是用到的一些屏幕空间后处理效果,部分如Lens Flare需要为卡渲风格做特定调整(这里使用与bloom类似的方式提取的高光区域作为输入,然后进行多次不同方向上的卷积并应用色彩调制来获得最终结果)
这里展示的是CG画质的渲染效果,看起来还是挺不错的
为了得到高画质的场景效果,崩坏3采用的策略是在PBR的基础上对其做风格化调整,使之表现接近动画风格:
- 对于色彩的做了卡通化调整
- 对于物体材质细节根据需要进行强调或省略
- 结合使用图像空间的勾线来强调物体边缘
这里是效果展示。
角色的表情是基于blendshape制作实现的:
- 将眼睛,嘴巴和眉毛拆分为单独的部件,分别控制其表情变化
- 自研了一套面部表情插件,用于实现表情动画的及语音嘴型的自动映射
- 预定义了一个包含丰富表情的集合,在角色交互的时候根据需要从中选用以驱动面部表情。
这里介绍了角色动画实现上的一些细节:
在Unity中如果使用humanoid作为动画导入方式,当关节处旋转角度较大时,关节处的形状可能无法满足项目组对动画品质的期望, 解决方案是:
1.1 在DCC中为每关节添加一个修正的blendshape,并将之一并导入到Unity以防止关节形变
1.2 由一个脚本来根据关节旋转角度来对形状进行插值混合,为了确保效果,每个关节会需要分别制作两个blendshape,一个用于90度,另一个用于140度以对关节形变补偿
- 另一种解决方法是使用额外的骨骼进行关节修正,这种方法更容易制作,但是对于结构细节的优化效果不如上一种使用blendshape的方案
流体和破碎是使用alembic格式,或者EXR贴图作为载体从Houdini或其他DCC工具导入的顶点动画实现的。
基于贴图的顶点动画由于是在GPU上计算实现的,其执行效率及加载速度要快于alembic格式
实时卡通渲染在今后可以继续改进和完善的地方:
- 实现所有类型材质完全可定制的风格化渲染,目前崩3只在人物皮肤和服装渲染中的应用了笔刷以获得笔触效果,后续会考虑将其扩展到整个场景的渲染,比如实现类似新海诚式的场景风格,以呈现有着独特且统一的风格化动画风格渲染。
- 进一步提高模型的渲染精度,最终期望实时呈现CG级的模型精度,比如可以考虑使用geometry shader或预烘培displacement map进行动态自适应的曲面细分,相比直接导入原始高模,它可以极大减少资源导入的开销和提升运行效率
- 优化整套流程解决方案,使之更易于实时调整和编辑,进一步提升运行效率以适合在游戏中使用
参考
[1]. Achieving high quality Anime style rendering on Unity
[2]. 米哈游技术总监首次分享:移动端高品质卡通渲染的实现与优化方案