Animoji是2017年9月13日苹果发布的新款手机iPhone X上的增强现实(AR)表情包。其使用面部识别传感器来检测用户面部表情变化,同时用麦克风记录声音,并最终生成可爱的3D动画表情符号。2018年6月5日,2018年苹果WWDC开发者大会上,苹果为Animoji引入了一项新技术——舌头感知Tongue Detection,可以感知用户吐舌头的表情,让表情更可爱。下面是一个demo视频:

我们基于Filament渲染引擎也实现了Animoji能力,下面是两个示例素材demo:

本文重点介绍一下Animoji素材和格式及其在Filament中的实现。

Filament简介

Filament是Google非官方支持的开源跨平台3D引擎渲染库,现在用于Android设备上的Sceneform 库中。它提供了基于物理的3D渲染能力。

Filament的优势:

  • 提供了基于物理(PBR)的渲染能力(Gameplay不支持基于物理的渲染)。
  • 安装包较小,具体大小见后面详细数据。
  • 跨平台(Android, iOS, PC),使用glTF/glb标准作为素材格式,支持各种设计工具导出素材。
  • 工具链较完整,部分支持自定义shader素材。

Filament从OpenGL ES 3.0开始支持,目前也有实验性质的Vulkan和Metal实现。下面是一些Filament和Gameplay的渲染效果对比:

Animoji能力技术方案

Animoji能力一般有两种实现方案:

  1. 直接利用人脸点位作为模型顶点坐标,根据3D人脸检测点位变化直接映射到模型点位变化,实现3D模型跟随人脸表情变化的效果。如下图所示:

    这种实现方式的优点在于它可以根据人脸点位完整表现用户表情变化,缺点也很明显:用户人脸点位的位置意义一般比较固定,导致模型设计很单一;人脸点位和模型点位强耦合,需要精心设计人脸点位和模型点位的映射关系,极大地限制了设计师的才能发挥。

  2. 使用多组模型顶点坐标,根据检测表情值计算加权顶点坐标。我们以张嘴表情为示例,实现效果如下图所示:

    其中前两幅图是设计师给到的两组模型顶点坐标,分别对应张嘴表情值为0(最小)和1(最大)时的模型效果。在表情检测SDK获取到张嘴表情值后,我们可以根据张嘴表情值的大小,对前两组顶点坐标加权计算出对应表情值的模型顶点坐标,如图3和图4分别对应张嘴程度为0.3和0.6时计算得到的加权顶点坐标位置对应的模型效果。

    这种实现方式的优点是它完全解耦了用户表情值和设计师对素材的设计,可以充分发挥设计师的想象力和创造性,效果表现及模型表情是否丰富则取决于表情检测的表情值是否丰富以及素材模型的复杂程度,表情的精细程度依赖表情检测的准确度和模型准确度。

在AEKit SDK中我们选择了第二种技术方案,因为它可以充分释放设计师的发挥空间。下面就重点介绍一下Filament中Animoji效果的实现方案。

Animoji素材配置及格式

Filament引擎使用glTF/glb作为模型输入格式。glTF(GL Transmission Format)号称3D界的JPEG,是一种标准的3D格式定义规范,用于高效地传输和加载3D场景和模型。glTF 最大限度地减少了3D资源的大小以及解压缩和使用这些资源所需的运行时处理。

下面我们简单介绍一下glTF的格式定义,先看一下结构图:

上图是glTF的一个大概结构,分为三大块:

  1. .gltf对应的json是一个配置描述,包括该模型的节点层级、材质信息、相机位置、动画等相关逻辑结构。
  2. bin文件对应这些对象的具体数据信息,包括顶点和索引数据、动画配置和骨骼动画配置数据。大块内容可以以Base64的编码内迁到文件中,方便拷贝和加载,也可以以URI的外链方式,侧重复用性。
  3. png、jpg等纹理图片。

下面是glTF格式中各个不同配置的一个大纲说明,有兴趣的同学可以详细了解一下各项配置的详细说明。其中我们Animoji能力实现使用的就是红框里标识出来的morph动画配置。

Morph Target配置说明

从版本 2.0 开始,glTF 支持对Mesh坐标的Morph Target定义。Morph Target存储特定Mesh属性的位移或偏移。在运行时,这些偏移可以按照不同的权重添加到原始Mesh属性中,以实现对某些Mesh坐标的动画变化。Morph Target在角色动画中经常使用,例如编码虚拟人物的不同面部表情。

下面是拥有两个Morph Target的Mesh坐标的简单示例:

{..."meshes":[{"primitives":[{"attributes":{"POSITION":1},"targets":[{"POSITION":2},{"POSITION":3}],"indices":0}],"weights":[1.0,0.5]}],...
}

我们简单解释一下这里的字段定义。渲染一个Mesh的基本几何体是三角形,三角形的顶点坐标是由mesh.primitives里的POSITION定义的。mesh.primitiveMorph Target是将"POSITION"属性映射到包含每个顶点的偏移的accessor对象的一个Map。以下图图示来说明,黑色表示初始三角形几何体,红色表示第一个Morph Target对应的位移,绿色表示第二个Morph Target对应的位移:

Mesh的weights属性定义了这些morph target对应的偏移以怎样的权重叠加到初始的几何坐标中,计算方法就是将初始的几何坐标加上以weights权重线性叠加morph target对应的几何坐标,得到的最终结果就是当前时刻的几何坐标数值,如下面的伪代码所示:

renderedPrimitive.POSITION = primitive.POSITION + weights[0] * primitive.targets[0].POSITION +weights[1] * primitive.targets[1].POSITION;

另外glTF文件中还定义了animation字段说明morph targets的weights权重随时间的变化情况,下面是一个简单的glTF的animation配置示例:

{..."animations":[{"samplers":[{"input":4,"interpolation":"LINEAR","output":5}],"channels":[{"sampler":0,"target":{"node":0,"path":"weights"}}]}],...
}

其中channels.target代表当前动画配置对应哪个node及node的什么属性发生变化,sampler对应一个时间和属性的映射表,如下所示:

Time Weights
0.0 0.0, 0.0
1.0 0.0, 1.0
2.0 1.0, 1.0
3.0 1.0, 0.0
4.0 0.0, 0.0

动画配置表明应用到morph target偏移的weights权重随时间的线性插值变化,每个时间点,Mesh渲染的顶点坐标都会对应更新,下面是1.25秒时计算得到的顶点坐标:

对Animoji功能来说,顶点坐标变化是通过表情系数控制而非动画配置,所以动画配置在Animoji功能中并没有用到。下面我们重点介绍一下Filament中如何利用Morph Target配置实现对模型的表情控制。

Morph Animation的GPU实现

Filament中Morph Animation就是基于上文描述的weights的计算方式实现的,下面是部分关键代码:

//prevIndex:当前时间对应的上一关键帧index
//nextIndex:当前时间对应的下一关键帧index
case Channel::WEIGHTS: {const int weightsPerTarget = sampler->values.size() / times.size();float4 weights(0, 0, 0, 0);const float* srcFloat = (const float*) sampler->values.data();for (int component = 0; component < std::min(4, weightsPerTarget); ++component) {if (sampler->interpolation != Sampler::CUBIC) {//对上下两关键值weights数据进行线性插值float previous = srcFloat[prevIndex * weightsPerTarget];float current = srcFloat[nextIndex * weightsPerTarget];weights[component] = (1 - t) * previous + t * current;} else {float vert0 = srcFloat[prevIndex * weightsPerTarget * 3 + 1];float tang0 = srcFloat[prevIndex * weightsPerTarget * 3 + 2];float tang1 = srcFloat[nextIndex * weightsPerTarget * 3];float vert1 = srcFloat[nextIndex * weightsPerTarget * 3 + 1];weights[component] = cubicSpline(vert0, tang0, vert1, tang1, t);}++srcFloat;}auto renderable = renderableManager->getInstance(channel.targetEntity);renderableManager->setMorphWeights(renderable, weights);continue;
}

通过上面的逻辑计算得到当前时间节点weights权重后,通过renderableManager传递weights数据到vertex shader中对顶点数据进行插值,我们可以简单看一下shader中的关键代码:

layout(std140) uniform ObjectUniforms {mat4 worldFromModelMatrix;mat3 worldFromModelNormalMatrix;vec4 morphWeights;  //上层传入的weights权重参数
} objectUniforms;vec4 getPosition() {vec4 pos = mesh_position;#if defined(HAS_SKINNING_OR_MORPHING)//基于weights对顶点坐标加权if (objectUniforms.morphingEnabled == 1) {pos += objectUniforms.morphWeights.x * mesh_custom0;pos += objectUniforms.morphWeights.y * mesh_custom1;pos += objectUniforms.morphWeights.z * mesh_custom2;pos += objectUniforms.morphWeights.w * mesh_custom3;}if (objectUniforms.skinningEnabled == 1) {skinPosition(pos.xyz, mesh_bone_indices, mesh_bone_weights);}#endifreturn pos;
}

我们可以看到,shader中morphWeights参数是一个4维向量,即对一次mesh渲染来说,最多只能传入4个morphWeights参数。这是因为vertex shaderattribute参数的个数是有限制的,最小支持个数为16,而Filament已经预留了一些attribute参数作为PBR渲染使用,所以留给morphWeights可使用的attribute参数就不多了。下面是Filament中attribute参数的占位定义:

/*** Vertex attribute types*/
enum VertexAttribute : uint8_t {// Update hasIntegerTarget() in VertexBuffer when adding an attribute that will// be read as integers in the shadersPOSITION        = 0, //!< XYZ position (float3)TANGENTS        = 1, //!< tangent, bitangent and normal, encoded as a quaternion (float4)COLOR           = 2, //!< vertex color (float4)UV0             = 3, //!< texture coordinates (float2)UV1             = 4, //!< texture coordinates (float2)BONE_INDICES    = 5, //!< indices of 4 bones, as unsigned integers (uvec4)BONE_WEIGHTS    = 6, //!< weights of the 4 bones (normalized float4)// -- we have 1 unused slot here --CUSTOM0         = 8,CUSTOM1         = 9,CUSTOM2         = 10,CUSTOM3         = 11,CUSTOM4         = 12,CUSTOM5         = 13,CUSTOM6         = 14,CUSTOM7         = 15,// Aliases for vertex morphing.MORPH_POSITION_0 = CUSTOM0,MORPH_POSITION_1 = CUSTOM1,MORPH_POSITION_2 = CUSTOM2,MORPH_POSITION_3 = CUSTOM3,MORPH_TANGENTS_0 = CUSTOM4,MORPH_TANGENTS_1 = CUSTOM5,MORPH_TANGENTS_2 = CUSTOM6,MORPH_TANGENTS_3 = CUSTOM7,// this is limited by driver::MAX_VERTEX_ATTRIBUTE_COUNT
};

可以看到,除了POSITIONTANGENTS等预留的attribute参数,剩下可用的用户可自定义的attribute参数就8个(空了一个未使用),而一个morph顶点坐标数据的同时包含POSITIONTANGENTS两个变量,所以一次渲染就只能传入8 / 2 = 4morph weights参数了。而对我们表情检测SDK来说,对每帧图像数据可以检测出52个表情值,常见的Animoji素材也包含有十数个表情值变化。受限于vertex shader规范限制,4个morph weights是远远不能满足要求的,所以我们参照GPU的计算逻辑实现了CPU版本的Morph Animation功能。

Morph Animation的CPU实现

Morph Animation在CPU实现相当于把shader里的相关数据结构和计算逻辑在C++层实现。下面是对应相关数据结构的关键代码:

//一个POSITION坐标对应的数据结构
struct MorphBaseData{uint32_t offset;       //glb buffer对应的accessor offsetuint32_t size;            //glb buffer对应的accessor sizevoid** data;        //指向glb buffer数据的指针std::vector<float> verticesData;   //解析后得到的byte流转换为float数组,对应weights里的POSITION顶点数据
};/* Morph Bindings for calculate position in CPU */
struct MorphBinding {uint8_t bufferIndex;  //Morph对应的顶点坐标对应的VertexBuffer里的Indexfilament::VertexBuffer* vertexBuffer;        //传入的vertexBufferMorphBaseData baseData;        //基准POSITION数据std::vector<MorphBaseData> morphDatas;      //Morph weights对应的POSITION数据std::vector<float> result;        //加权计算后的POSITION数据int getMinMorphSize() {int minSize = baseData.verticesData.size();for(MorphBaseData data : morphDatas){if(minSize > data.verticesData.size()){minSize = data.verticesData.size();}}return minSize;}//根据当前时间计算得到的weights,计算加权后的POSITION数据void updateMorphWeights(filament::Engine *engine, const std::vector<float>& weights){int size = getMinMorphSize();int weightsSize = std::min(weights.size(), morphDatas.size());if(result.size() < size){result.resize(size);}for(int i = 0; i < size; i++){result[i] = baseData.verticesData[i];for(int j = 0; j < weightsSize; j++){result[i] += morphDatas[j].verticesData[i] * weights[j];}}}void setMorphWeights(filament::Engine *engine){vertexBuffer->setBufferAt(*engine, bufferIndex,filament::VertexBuffer::BufferDescriptor(result.data(), result.size() * sizeof(float)));}
};

在数据解析时,glTF素材里的buffer数据大小端排列方式与c++层默认的大小端排列方式一致,所以我们可以把buffer对应的byte流数据直接拷贝到vector指向的数组中,然后计算时以4个byte作为一个float解析即可,避免一个个float解析导致性能较差的问题。关键代码如下所示:

void ResourceLoader::loadVerticesData(MorphBaseData& morphBaseData){if(morphBaseData.size <= 0){return;}std::vector<float> &dstVec = morphBaseData.verticesData;dstVec.resize(morphBaseData.size / sizeof(float));auto dstPos = (uint8_t*) dstVec.data();const uint8_t* srcBuffer = morphBaseData.offset + (const uint8_t*) *morphBaseData.data;memcpy(dstPos, srcBuffer, morphBaseData.size); //byte流数据直接铺平放入vector指向的空间
}

表情值与Morph Weights的对应

我们正常看到的Animoji素材,人脸表情值和素材表情是完全对应的,比如用户眨左眼,Animoji模型也眨左眼。而Morph Animation的实现解耦了人脸表情值和素材的关系,理论上来说,完全可以做到用户眨左眼,而Animoji模型眨右眼。在实际的实现中我们也是这样做的,只要配置好表情值和Animoji素材中Morph Weights的映射关系即可。下面是一个典型的例子:

//expressionOrderList的顺序与GLB里Morph Weights对应的顶点顺序一致
"expressionOrderList": ["jawOpen","eyeBlinkLeft","eyeBlinkRight"
],
"expressionConfigList": [{"shapeName": "jawOpen",},{//左眼表情控制Animoji右眼眨"shapeName": "eyeBlinkLeft","controlledName": "eyeBlinkRight"},{//右眼表情控制Animoji左眼眨"shapeName": "eyeBlinkRight","controlledName": "eyeBlinkLeft"}
]

实际上,我们可以通过素材配置实现任意表情控制Animoji素材的任意Morph Animation表情。

总结

本文介绍了Animoji能力在Filament中的技术实现,该功能目前已集成在产品拍摄模块中。Morph Animation作为3D素材的几种动画能力之一,不仅在Animoji中适用,在3D素材动画效果的设计中也是一种很常见的方式,有兴趣的同学可以参考引用深入了解。


参考文献

Animoji
GLTF Tutorial

基于Filament引擎的Animoji效果实现相关推荐

  1. 【HIMI转载推荐之三】基于Cocos2dx引擎UI扩展引擎包[cocos2d-x-3c]

    [前言点评] 此篇主要作者:jason-lee-lijunlin  基于Cocos2d-x引擎进行封装的UI框架的扩展包. 此文章Himi已经仔细看过,总体来说是篇很好的文章,是给使用-x引擎的童鞋们 ...

  2. php配合jade使用,详解基于模板引擎Jade的应用

    本文小编就为大家分享一篇基于模板引擎Jade的应用详解,具有很好的参考价值,希望对大家有所帮助.一起跟随小编过来看看吧,希望能帮助到大家. 有用的符号: | 竖杠后的字符会被原样输出 · 点表示下一级 ...

  3. 基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统

    基于Unity引擎利用OpenCV和MediaPipe的面部表情和人体运动捕捉系统 前言 项目概述 项目实现效果 2D面部表情实时捕捉 3D人体动作实时捕捉 补充 引用 前言 之前做的一个项目--使用 ...

  4. Linux aarch64交叉编译之 Google filament引擎

    对于filament的编译.该文章的目标是编译一套aarch64 Linux Debian嵌入式版本上可以运行的库,本来想在网上找些现成的,然而 资料少的一13,接下来就开始趟坑.老套路,先把linu ...

  5. 基于 Web 引擎技术的 Web 内容录制

    随着基于WebRTC技术的Web应用快速成长,记录web在线教育.视频会议等场景的互动内容并对其准确还原越来越成为一项迫切需求.在主流浏览器中,通常基础设施部分已实现了页面渲染结果的采集及编码.开发者 ...

  6. RTE 大会回顾 | 基于 Web 引擎技术的 Web 内容录制

    随着基于WebRTC技术的Web应用快速成长,记录web在线教育.视频会议等场景的互动内容并对其准确还原越来越成为一项迫切需求.在主流浏览器中,通常基础设施部分已实现了页面渲染结果的采集及编码.开发者 ...

  7. 使用 Sticky-Kit 实现基于 jQuery 的元素固定效果

    元素固定效果在网页中应用得很多,比较常见的使用场景有改进导航,显示广告.Sticky-Kit 是一个非常方便的 jQuery 插件,简化了创建/管理粘元素,有复杂的使用功能.这些功能包括:处理多个固定 ...

  8. 19_clickhouse,数据查询与写入优化,分布式子查询优化,外部聚合/排序优化,基于JOIN引擎的优化,SQL优化案例,物化视图提速,查询优化常用经验法则,选择和主键不一样的排序键,数据入库优化

    25.数据查询与写入优化 25.1.分布式子查询优化 25.1.1.分布式表的IN查询示例1(普通IN子查询.IN子查询为本地表) 25.1.2.分布式表的IN查询示例2(普通IN子查询.IN子查询为 ...

  9. 推荐20款基于 jQuery CSS 的文本效果插件

    jQuery 和 CSS 可以说是设计和开发行业的一次革命.这一切如此简单,快捷的一站式服务.jQuery 允许你在你的网页中添加一些真正令人惊叹的东西而不用付出很大的努力,要感谢那些优秀的 jQue ...

最新文章

  1. java socket modbus_Java modbus tcp 编程有懂得吗?给个示例看看。。。十分感谢。
  2. Android 仿微信小视频录制
  3. 关于$_SERVER['PHP_AUTH_USER']
  4. 交换机怎么使用vtp
  5. android RSA加密
  6. 接受数据,table列表,外加判断
  7. web.xml(8)_jsp-config
  8. 对抗神经机器翻译:GAN+NMT 模型,中国研究者显著提升机翻质量
  9. 一步一步搭建vue项目
  10. 博客园签名档图片圆角美化
  11. 【技巧】解决谷歌浏览器自带的谷歌翻译无法自动翻译问题
  12. 谷歌2019 学术指标发榜:CVPR首次进入Top 10,何恺明论文引用最高!
  13. jenkins中文语言设置
  14. 机器学习 | MATLAB实现BP神经网络newff参数设定(下)
  15. ASFG - AI可以帮你自动生成字幕文件
  16. 实践+收藏版——Linux 性能优化知识点总结大全!
  17. 茶楼收银系统应该如何选择?
  18. 乐视三合一摄像头和kinect_#三合一体感摄像头评测#体验篇
  19. 2021年美容师(初级)考试及美容师(初级)找解析
  20. 鸟哥的Linux私房菜(服务器)- 第一章、架设服务器前的准备工作

热门文章

  1. 中缀向后缀转换表达式
  2. 攻防世界--杂项misc-János-the-Ripper--题解
  3. 归一化的matlab实现
  4. mysql通过idb文件,恢复数据库
  5. arm linux fpu,多媒体处理,利用ARM NEON/FPU提升performance
  6. np.clip的使用方法
  7. 黑盒测试(什么是黑盒测试 黑盒测试的优缺点 黑盒测试中的测试方法)
  8. 【Python应用】自制截图取词小工具-- 解锁文字识别新姿势
  9. LVGL - 在STM32上的移植
  10. CheckBoxList详细用法