
不知什么时候,捏脸几乎已经成为游戏的标配,用户通过捏脸系统创建出个性化角色,彰显独特的形象,增强了沉浸感。从技术角度讲,捏脸也有很多种实现方式,比如:
BlendShape可以很好的表现不同的面部表情,但是制作工作量大,一般CG,影视等应用较多。
AI智能捏脸现在发展势头很猛,通过对输入的图片做AI智能图像识别,提取出面部特征,再根据特征定制角色Mesh,从而快速完美的还原现实世界中的人物形象。具体实现可以参考 面由 AI 生|虚拟偶像“捏脸”技术解析
今天我们主要讨论骨骼捏脸以及在Unity中的实现。说到骨骼捏脸,就要先了解一下骨骼,骨骼动画,蒙皮Mesh,权重这些名词。这里假设读者已经知道这些概念,从而可以尽快进入主题。
骨骼捏脸也有很多实现方式,比如:
第一种 是先计算好捏脸骨骼和原始骨骼的差异值,然后在每帧动画后,再给每根骨骼应用一次这个差异值,比如捏脸的时候缩小了脸部骨骼0.5倍。 当骨骼动画的每帧完成后,动画数据会把脸部骨骼设置回原本的值,这时候在LateUpdate中,把脸部骨骼再缩小0.5倍,从而达到还原捏脸效果的目的。
优点: 实现简单
缺点: 由于每帧都要做额外的补偿计算,所以效率不高。
第二种 由于把影响蒙皮的骨骼和驱动动画的骨骼进行了分离,动画数据不直接改变蒙皮骨骼的数据,捏脸时指定的变化,不会被动画数据覆盖,所以,只要调整蒙皮骨骼就能达到捏脸效果。
优点: 实现更简单
缺点: 增加了额外的骨骼,影响效率
参与动画的骨骼不能进行调整(会被骨骼动画还原),从而限制了捏脸的自由度,对捏脸效果产生不利影响。
第三种: 修改骨骼Bindposes达到捏脸效果。
优点: 只计算一次,效率高。
缺点: 实现稍微复杂,需要良好的数学基础。
由于第三种优点最为突出,我们今天就来讨论它是如何实现的。
首先说一下对骨骼的简单理解:
骨骼就是一系列具有父子关系的Transform。由于骨骼具有父子关系,所以对父骨骼的修改会影响其子骨骼,比如抬手臂会带动手指骨骼。

骨骼动画就是在关键帧中指定骨骼的transform。 位置,旋转,缩放.。关键帧之间通过插值得到中间状态,从而提高动画流畅度。

前两者很好理解,最后一点不是那么直观,而且是我们捏脸的关键,所以我们详细介绍一下。
TPose是骨骼的一个初始状态,模型和骨骼的绑定就是在这个状态下进行的,所谓的绑定,就是指定模型的顶点受哪些骨骼影响,每个骨骼影响的权重是多少,这些一般都是美术在美术工具软件中做好,导入到Unity中直接使用,我们为了说明原理,直接用一个蓝色小球代表模型上的一个顶点。把他和骨骼BN_20绑定。权重为1。也就是它仅受BN_20影响。

现在这个顶点(蓝色小球),位于BN_20上方一个单位,但是看右边面板注意到,Mesh以及其上的Vertex和骨骼并没有父子关系,所以现在变换骨骼,顶点vertex并不会跟随移动。我们需要把两者关联起来(绑定).
假设BN_10向右移动了一个单位(会带动子骨骼BN_20一起向右),那蓝色小球也应该跟随绑定的骨骼BN_20一起向右移动一个单位,但是如果BN_10围绕Z轴旋转90度,蓝色小球又该在什么位置呢?他们是怎么绑定的,有什么规律呢?
答案就是顶点相对于绑定的骨骼,相对位置不变,也可以描述为,顶点在绑定骨骼的坐标系下,坐标不变。这样BN_20.transform.position.x += 1.那Vertex.transform.position += 1(世界坐标系下). 如果BN_10绕z轴旋转了,那BN_20的坐标系也跟着旋转了,但是顶点在该坐标系下,位置还是不变。

上图是理想的位置,它是怎么计算来的呢?上面说了顶点和骨骼相对位置不变,就是顶点在骨骼坐标系下坐标位置不变。那我们先求出在初始位置(TPose)下,顶点在骨骼BN_20坐标系下的坐标位置。直接转换坐标系不好转换,但是我们可以先把vertext坐标转换到世界空间,然后再由世界空间转换到BN_20空间。

Vector3 vertexPositionInBone_20 = (BN_20.transform.worldToLocalMatrix * mesh.transform.localToWorldMatrix).MultiplyPoint3x4(vertext.transform.localPosition);
好了,现在顶点相对于绑定骨骼的相对位置已经计算出来了,接下来就保证骨骼在各种变换后,顶点的vertextPositionInBone不变就好了。反过来想,假设BN_20变换到了新的位置,只要把BN_20本地坐标下的vertexPositionInBone转换到世界坐标系下得到位置A,然后,把vertext的世界坐标移动到A,就能保障相对位置不变了。

对应代码如下:
vertext.transform.position = bone.transform.localToWorldMatrix.MultiplyPoint3x4(vertexPositionInBone_20);
这里要明确一点,图5中bone的矩阵是TPose时的矩阵,这个矩阵是固定不变的,图6中是骨骼变换后的矩阵,这个矩阵随着骨骼的变换而不断变化,从而驱动顶点不断变化。
说到这里基本把骨骼动画,蒙皮,权重(特例只受一根骨骼影响)的原理讲清楚了,但是读者还是不明白什么是Bindposes. 其实图5中最后一行,用括号括起来的部分就是骨骼的Bindpose, 很意外吧,原来早就认识了,只是不知道对方的名字,它的数学意义是 在TPose下,从模型的本地空间到骨骼的本地空间的转换矩阵,这里面每个词都是有意义的,首先一定是骨骼的初始状态(TPose下), 再就是从一个坐标系,到另一个坐标系的转换矩阵.
捏脸:
好了,啰里啰嗦一大堆,终于知道bindpose是啥了,那它和捏脸有什么关系呢?下面我们来说明,假设vertext是鼻子上的一个顶点,现在TPose下,这个顶点离骨骼BN_20一个单位远,如果我在TPose下,把vertex再多远离BN_20一个单位,会发生什么呢?发现鼻子变高了,vertext在BN_20坐标系下,坐标位置由(0,1,0)变成(0,2,0)了。 按照上面说的骨骼动画的原理,骨骼动起来,顶点相对于骨骼坐标位置不变,所以骨骼动画动的过程中,vertex相对于BN_20始终位于(0,2,0)的位置,这样,角色怎么动,鼻子始终是高的。这就是捏脸所需求的。下面看看具体计算过程:

图7中括号里面,就是骨骼新的bindpose.只要用这个bindpose,顶点就相对于骨骼始终在新的相对位置。
给Mesh赋值新的bindpose的代码如下:
//Dictionary<string,Matrix4x4> newBindposes; //这里面存储根据用户捏脸后计算出的新的Bindpose
SkinnedMeshRenderer smr = face.GetComponent<SkinnedMeshRenderer>();
//实例化一份新的mesh
Mesh mesh = GameObject.Instantiate<Mesh>(smr.sharedMesh);
Matrix4x4[] bindposes = mesh.bindposes;
Transform[] bones = smr.bones;
for (int i = 0; i < bones.Length; ++i)
{
bindposes[i] = newBindposes[bones[i].name];
}
mesh.bindposes = bindposes;
smr.sharedMesh = mesh;
OK,现在看看捏脸后的效果吧:



感兴趣的朋友可以自行推导一下,你会发现其中数学的乐趣。如果疑惑较多,我可以单开一章用来说明这块。
另外吐槽一下,Unity资源商店竟然没有找到轻量级捏脸的插件,是我的搜索方式不对吗?有知道的推荐个好用的给我呗,先行谢过!
【转载请注明出处】
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
目录1.AdmobSDK下载地址2.将下载好的unityPackagesdk导入到unity里编辑 3.解析依赖到项目中
Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg
通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复