WebGL高质量实时角色渲染

本期作者

背景

随着图形图像渲染技术的快速发展,如何在移动端呈现出高质量的数字人渲染效果,是实时渲染领域最主流的技术研究方向之一。对于B站移动端App而言,如果使用主流的实时渲染引擎如Unreal/Unity等,都会带来100-130M左右的安装包体积增量,进而增加应用安装和版本更新的成本。

针对该问题,我们选择了更为灵活轻量的WebGL渲染方案,将包体增量大幅降低至1M以内,同时借助Web天然的开箱即用特性,加速了业务需求在移动端落地的整体节奏。经过对Web渲染能力的行业调研,我们最终从众多的Web3D渲染引擎中选择了Three.JS。Three.JS作为一款轻量级的JavaScript 3D渲染库,具备强大的图形能力和广泛的社区支持,在数字人渲染方向能够给予我们一定的基础能力支持。

但如果只是使用Three.JS自带的PBR(Physically-Based Rendering)渲染,在偏CG和写实方向的数字人渲染效果上,很难达到令人满意的品质感。为了能够进一步还原商业实时渲染引擎Unreal的人物效果,同时兼顾好WebGL在移动设备上的性能和发热问题,我们在人物皮肤,瞳孔,抗锯齿,半透明等方向上做了深入的二次研发,提出了一套完整的高质量角色移动端WebGL渲染解决方案。本文将分享我们在探索和实现该方案的过程中遇到的挑战及最终的解决思路。

PBR优化

PBR(Physically-Based Rendering)是一种计算机图形学中的渲染技术,旨在模拟(近似)光线在现实世界中的物理行为,以实现更真实、逼真的渲染效果。因此我们的美术资产的制作采用了全套PBR流程,Three.JS内置的MeshPhysicalMaterial也对PBR做了材质支持,但我们发现Three.JS的PBR实现在移动端上依然存在性能瓶颈。所以针对以上问题我们给出了如下解决方案:

首先我们来看Cook-Torrance的BRDF模型:

其中Cook-Torrance镜面反射如下:

D表示法线分布函数(Normal Distribution Function)

G表示几何函数(Geometry function)

F表示菲尼尔函数(Fresnel function)

n表示法线方向,l表示灯光方向,v表示视线方向,h表示l与v的中间方向(halfway)

首先,我们使用 UE4 粗糙度的定义,将它用于以下所有方程中的α:

分布函数:

几何函数:

我们使用史密斯方法(Smith's method)将G_GGX作为子函数G_sub:

菲尼尔函数:

F0表示表明的基础反射率,一般我们采用vec3(0.04)

我们还可以将镜面反射分母与几何函数进行合并得到可见性项(Visibility):

最后我们得到了:

我们发现几何函数里有除法和求平方根,写成代码为:

有没有办法简化呢?根据SIGGRAPH 2015 Optimizing PBR for Mobile有关简化VF项:

最终我们的表达式:

加和乘,仅有一次除法。

与Three.JS内置PBR性能对比

我们设计了一个测试场景,仅渲染一个角色,同时去除了其他干扰,为了能体现本次性能差距,选用了3款机器进行测试帧生成时间(SOC由于发热等因素可能是一个区间):

机型 (处理器) ThreeJS PBR 优化 PBR 提升比例
iPhone 6 (A8) 20-22ms <16ms 20-33%
Redmi 6 Pro (骁龙625) 52ms 32ms 62%
华为 P20 (麒麟970) 10ms 7-8ms 25-42%

较好的机器在该场景下通常能跑上百帧,两者差距相对较小,这里就不一一列举。

皮肤优化

为了营造皮肤的通透感,我们调研了市面上主流的皮肤渲染方案,例如屏幕空间次表面散射(Screen Space Subsurface Scattering Skin Rendering)、可分离次表面散射(Separable Subsurface Scattering)、预积分次表面散射(Pre-Integrated Skin Shading)

以上方案除了预积分次表面散射,其它两种方案都要使用实时卷积模糊,开销非常高。而预积分次表面散射方案,根据SIGGRAPH 2011 Pre-integrated Skin Shading的分享,在完成LUT贴图和曲率贴图烘培的情况下,仅使用两次贴图采样即可模拟较好的次表面散射效果。

LUT贴图烘培

曲率贴图烘培

考虑到移动端性能和效果平衡取舍,所以采用了预积分次表面散射的方案:

左:未开启SSS 右:开启SSS

瞳孔优化

眼睛渲染一大难点是模拟瞳孔的一个自然的凹陷,如果没有处理好,从角色侧面观察瞳孔会突出,角色将失去神采。

瞳孔处理通常有两种方法,一种是将瞳孔和眼角膜分开建模,瞳孔向下凹陷,另一种做法是用视差方法模拟瞳孔凹陷。

Tda式初音ミク・アペンドVer1.10

考虑到美术制作流程上的复杂度,我们选用了后者,需要把眼球的前向量传递给Shader,然后把视线转换到切线空间对贴图进行采样,最后应用一张Mask贴图决定整个瞳孔的凹陷程度。

左:未开启瞳孔优化 右:开启瞳孔优化

渲染管线设计

我们遇到一些移动设备高分屏的问题,在IPhone13Pro上,内部分辨率高达1170×1992,根据我们的经验,这会造成手机发热与性能问题。

为了解决上述问题我们设计了一套离屏渲染管线,使用Threejs的EffectComposer申请了与屏幕分辨率不同的RenderTarget,并且在RenderScale=0.5的情况下,在大部分高分屏手机上取得了还不错的效果,兼顾了效果与性能。

具体管线如下:

与Three.JS默认直接画到画布上的管线做对比,我们的方案实现的功能更复杂,由于可以控制离屏渲染分辨率,性能与质量之间可配置的灵活度也更高。

抗锯齿

Three.JS支持硬件抗锯齿MSAA和屏幕空间抗锯齿SMAA,在最开始我们觉得这两种抗锯齿是够用的,但随着美术需求逐渐复杂,我们发现上述两种抗锯齿对于一些边缘光照产生的高频像素无能为力,并且在实现头发等效果时,发现MSAA仅对几何走样(Geometry Aliasing)有较好的效果,但对着色走样(Shading Aliasing)基本无效。

MSAA着色走样

我们尝试使用SMAA解决,但最终结果仍不理想。根据经验,着色走样需使用超采样(Super-Sampling)抗锯齿解决,但这样无疑又增加了渲染像素,导致性能问题。

有没有即不需要超采样,又可以解决着色走样的抗锯齿技术呢?答案是有的,我们采用时域抗锯齿(Temporal Anti-Aliasing),接下来简称为TAA。

在实践中发现,Three.JS上并没有直接可用的TAA,官方内置的TAA本质是个超采样,这并不符合我们的预期。根据上述背景,我们决定从零开发TAA。

摄影机抖动

TAA的核心思想是将多次采样的过程分布到每一帧当中,这里我们除了空间上均匀分布,还希望能在时间上均匀分布。这里根据SIGGRAPH2014 Unreal Engine 4 TAA的分享,我们使用低差异序列Halton(2, 3)作为子像素抖动偏移:

然后我们将这个抖动(Jitter)偏移调整到摄影机的投影矩阵上:

运动向量

尽管我们将采样分布到了每一帧上,但是保存多帧对于移动平台来说是不实际的,这时我们需要简化为只保存两帧,当前帧和历史帧。我们通过指数混合的方式将当前帧和历史帧进行不断累积。如果我们只是简单的将当前帧和历史帧混合,一旦摄影机或物体发生运动就会出现残影问题,我们还需要知道当前像素在上一帧所处的位置,这个过程称为重投影(Reprojection),我们使用运动向量(Motion Vector)重建上一帧当前像素的位置。

渲染运动向量:

这里需要注意的是,计算上一帧裁剪空间(Clip Space)位置时我们除了要传递上一帧的MVP矩阵给Shader外,我们还需要将上一帧的蒙皮骨骼矩阵一并上传(Three.JS使用的是GPU蒙皮)。为了节省性能,运动向量的RT我们使用了RGBA8格式,在输出时,还需要将两个Float打包进RGBA8中,在使用运动向量时,我们还需要进行相应的Unpack操作。

当然,在移动端渲染运动向量需要将场景中物体再次渲染一遍,这样开销在低端机上依然无法接受,所以在低端机上我们使用深度图进行位置重建:

具体思路是用逆VP矩阵将位置转换到世界空间,然后再算出上一帧的位置。当然这个方法的缺点是无法重建蒙皮动画的运动。

计算颜色包围盒

尽管进行了上述操作后,残影问题解决了一大部分,但是我们发现在平移物体的时候,依然会在背景上出现大量的鬼影(Ghosting)。

这是由于当前像素在上一帧并没有出现(被遮挡),在速度向量上也不会有记录。这里我们的解决办法是计算当前帧像素周围8个采样点的颜色,计算出最大最小颜色,形成一个颜色包围盒。我们还可以在YCoCg空间中获得更加精确的结果。然后我们就可以将历史帧的颜色限制在这个颜色包围盒范围之内。

方差裁剪

在高端设备或PC平台上,我们还参考了Nvidia GDC 2016上提出的方差裁剪(Variance Clipping)

这里我们的gamma值取2将会获得一个比较稳定的结果。

左:gamma 1.0 右:gamma 2.0

深度扩张

到目前为止,我们已经得到了比较好的效果,但我们移动摄影机会发现,物体边缘会有锯齿。

这里我们需要对当前像素的深度周围8个像素进行采样,得到一个最小值(离摄影机最近),然后得到该采样点的纹理坐标偏移。然后对速度向量采样时,应用这个偏移。这样相当于对速度向量离摄影机最近的部分进行扩张,这样就解决了物体边缘锯齿的问题。

抗闪烁

最后一个问题,也是最难解决的问题,就是TAA抖动的时候,造成一些像素当前帧被光栅化了,但到了下一帧又消失了,这就导致之前计算颜色包围盒时,历史帧的颜色与当前帧色彩差距过大,被裁剪掉了,这时就会出现非常明显的闪烁问题。

这里我们的解决办法是再多申请一张RT,记录上一帧的颜色,然后根据速度向量的差值混合当前帧和上一帧颜色。这个方法初衷是使用两帧颜色,尽可能还原更多高频信息。

左:单帧 右:2帧混合

我们可以观察到头发丝的结果也更加稳定,改善了发丝断断续续的情况。虽然目前画面仍有一些闪烁问题,但已经可以被接受。

左:TAA 中:MSAA 右:SMAA

抖动半透明

半透明渲染一直是光栅化渲染中的难题,纯半透明渲染又和渲染顺序息息相关,要解决顺序问题,我们可以在美术资产制作的时候严格按照顺序进行分组与拆分,但这样无疑对美术制作负担过重。如果我们使用类似深度剥离、权重混合等OIT技术,作为移动平台开销又太高了。至于像链表OIT WebGL不支持,我们即希望开销足够低,又可以无视排序问题,还可以获得不错的半透明效果,那有没有这样的解决方案呢?答案是有的:抖动半透明(Dithering Transparency)。值得一提的是,目前市面大多数3A游戏也在使用这个技术。

但请注意,这个技术需要配合TAA一起使用,如果单纯使用抖动技术并不能获得很好的效果。

此外关于抖动的pattern,规则抖动可以获得较平滑的结果,但是由于其原理没法保留背后的半透明信息,相当于覆盖掉了。我们的解决办法是在规则抖动中加入一些随机抖动:

*左:半透明裁剪 中:规则抖动 右:规则抖动+随机抖动*

软阴影

一个好的阴影效果,将会大大提升画面的真实度,Three.JS内置4种阴影类型:

  1. 未过滤,锯齿感严重,无法使用
  2. PCF相比上面多了几次采样,但依然有锯齿感
  3. VSM对于平面投影效果非常不错,但是对于复杂曲面物体,瑕疵较多
  4. PCF Soft虽然消除了锯齿感,但是半影范围无法调整

综上所述Three.JS内置的阴影均不符合预期效果,我们的诉求是不要有明显锯齿感,且可以有较大的半影范围。

泊松分布

我们使用泊松盘(Poisson Disc)对阴影贴图进行采样,并且每次会随机旋转一个角度,来消除规则pattern。最后我们配合TAA消除噪点:

左:旋转泊松盘 右:泊松盘+TAA

泛光

一般来说我们的显示设备通常不支持HDR(高动态范围),于是我们需要模拟光线在薄膜中的次表面散射(胶片、镜头滤光片、视网膜等)。在PBR管线下,镜面反射的动态范围通常非常高,此技术可以帮助物体表现相对亮度,或给LDR图像添加真实感。

Three.JS中包含多个内置的Bloom、Glow效果,但效果往往是一种简单的模糊,不符合我们对高动态范围亮度泛光效果的预期。

基于物理的泛光

(Physically Based Bloom)流程如下:

首先对原画面图做一个阈值处理,过滤出亮度超过阈值的颜色,然后对这个颜色进行降采样并进行模糊,每次下采样的纹理尺寸将是之前的一半,一般我们迭代7-8次,由于迭代次数越多,最终Bloom能溢出的范围将越大。达到最低的mip等级后,我们就可以进行上采用,这里需要注意,所有mip等级都需要持久保存,将当前等级和前一级模糊的图像进行混合,混合因子也决定了溢出范围。

泛光透明穿透

完成上采样以后,最后一步是和原图颜色混合,一般来说在Linear色彩空间下使用加法即可。

由于我们的项目还需要透明背景,仅仅对Alpha通道进行模糊处理依然会有光晕黑边问题。

这里我们将最终Bloom颜色转换到HSV色彩空间,将V值保存为Alpha,然后将V值设为1。

总结

通过上述的技术方案重构和优化,我们在Web上实现了符合预期的3D数字人渲染效果。横向对比头部的移动端数字人竞品,在CG和仿真风格领域,我们具备较大的竞争优势,特别是在人物关键特征的还原和光影效果的复现上。在未来的迭代中,我们将继续在捏脸换装,人物面部/身体动画,互动特效方向上继续深耕效果优化,打造更具行业竞争力的数字人个性化动态渲染解决方案。

参考文献

Zioma, R. (2015). Optimizing PBR [PowerPoint slides]. community.arm.com/cfs-file/__...

Jimenez, J., & von der Pahlen, J. (2013). Next Generation Character Rendering [PowerPoint slides]. www.iryoku.com/stare-into-...

Penner, E. (2011). Pre-Integrated Skin Shading [PowerPoint slides]. advances.realtimerendering.com/s2011/Penne...

Karis, B. (2014). High Quality Temporal Supersampling [PowerPoint slides]. de45xmedrsdbp.cloudfront.net/Resources/f...

OpenGL Tutorial. Tutorial 16 : Shadow mapping. www.opengl-tutorial.org/intermediat...

Christensen, A. P. (2022). Physically Based Bloom. learnopengl.com/Guest-Artic...

相关推荐
烛阴3 小时前
从“无”到“有”:手动实现一个 3D 渲染循环全过程
前端·webgl·three.js
WebGISer_白茶乌龙桃16 小时前
Cesium实现“悬浮岛”式,三维立体的行政区划
javascript·vue.js·3d·web3·html5·webgl
烛阴1 天前
拒绝配置地狱!5 分钟搭建 Three.js + Parcel 完美开发环境
前端·webgl·three.js
WebGISer_白茶乌龙桃2 天前
Vue3 + Mapbox 加载 SHP 转换的矢量瓦片 (Vector Tiles)
javascript·vue.js·arcgis·webgl
ThreePointsHeat6 天前
Unity WebGL打包后启动方法,部署本地服务器
unity·游戏引擎·webgl
林枫依依7 天前
电脑配置流程(WebGL项目)
webgl
冥界摄政王9 天前
CesiumJS学习第四章 替换指定3D建筑模型
3d·vue·html·webgl·js·cesium
温宇飞11 天前
高效的线性采样高斯模糊
javascript·webgl
冥界摄政王12 天前
Cesium学习第一章 安装下载 基于vue3引入Cesium项目开发
vue·vue3·html5·webgl·cesium
光影少年14 天前
三维前端需要会哪些东西
前端·webgl