今年五一假期,心血来潮想研究Faceu的吐彩虹、3D装饰和换脸等功能是怎样实现的。
期间进行了多次尝试,实现的效果都相差甚远。
直到最近有幸拜读两位大神的博客专栏,得到了很大的启发。链接如下:
对于有相同兴趣的朋友,强烈建议先阅读上述大神的专栏。
在大神的博客的启发下,终于实现了稍微有一点接近的效果:
项目链接: https://github.com/SimonCherryGZ/ARCamera
如果觉得效果还算过得去,那么请继续往下看。
Rajawali是Android端OpenGL ES 2.0/3.0 引擎。使用它可以很方便地进行3D应用开发。
但本项目使用Rajawali是无奈之举。因为渲染照相机画面的SurfaceView,和Rajawali渲染3D模型的SurfaceView不是同一个。
那么拍照和录像的时候就需要将两个SurfaceView的数据进行合成。
其实大神的两篇文章也有介绍如何自己实现加载3D模型的功能:
http://blog.csdn.net/junzia/article/details/54300202
http://blog.csdn.net/junzia/article/details/58272305
如果是自己实现加载3D模型的部分,那么应该可以把3D模型渲染在同一个SurfaceView上。
但是笔者在运行大神的代码时却不能正常工作。而看大神的博客评论,也是有博友能够正常运行的。
那么应该不是代码的逻辑问题,而可能是手机型号问题。
由于笔者急于实现功能,在这个问题上也没有去深究,因此选择了使用Rajawali。
Rajawali提供了丰富的官方示例,wiki页面也有详细的教程,所以其基本的使用方法这里就不重复了,本文主要是分享一下使用上的经验。
下面用到的例子都修改自官方示例,并且只贴出关键部分的代码,因此建议您先阅读官方示例代码。
在Rajawali初始化场景的方法中,加载人脸模型和眼镜模型,运行后发现不符合预期效果:
我们用3D建模软件(我用的是Blender)导入这两个模型来观察:
这样一看就明白了,因为这个眼镜模型比人脸模型大得多,并且还是平躺的。
此时,我们当然可以在Rajawali里面调整眼镜模型的大小、位置和旋转角度,来达到我们的要求。
但是这样就有点麻烦了,比如设置大小,应该要缩小多少才能适配我们的人脸模型呢?
更方便的做法是直接在Blender里面,对照着人脸模型来调整这个眼镜模型的大小和位置:
导出调整过后的眼镜模型,在Rajawali中加载这个新模型:
protected void initScene() {
try {
// 加载人脸模型
LoaderOBJ parser1 = new LoaderOBJ(mContext.getResources(), mTextureManager, R.raw.obama_face_obj);
parser1.parse();
Object3D obamaFace = parser1.getParsedObject();
obamaFace.setScale(0.15f);
obamaFace.setY(-0.5f);
// 加载眼镜模型
LoaderOBJ parser2 = new LoaderOBJ(mContext.getResources(), mTextureManager, R.raw.glasses2_obj);
parser2.parse();
Object3D glasses = parser2.getParsedObject();
// 已经调整到跟人脸模型是1:1的比例,所以使用跟人脸模型一样的参数就可以了
glasses.setScale(0.15f);
glasses.setY(-0.5f);
// 添加这两个模型到场景中
getCurrentScene().addChild(obamaFace);
getCurrentScene().addChild(glasses);
} catch (ParsingException e) {
e.printStackTrace();
}
}
这个眼镜模型其实还好,未调整参数之前还能看到一部分,根据局部的画面,你也能猜到它的状态。
但有一些模型偏离坐标原点,直接加载的话完全看不见。比如这个:
所以遇到模型不显示的情况,主要的原因还是因为模型的大小和位置问题。
另外,上面的代码中,为了让模型能在画面居中显示,人脸模型和眼镜模型都缩小到0.15倍,并且在Y轴方向移动了-0.5个单位。
如果你懒得设置这两个参数,也可以用Blender把模型的大小位置调得刚刚好,就不用再写多余的设置。
仅仅显示模型没什么意思,肯定要让它动起来。
Rajawali提供的示例中,AccelerometerFragment就演示了如何利用手机加速度计来旋转模型。
这个示例中加载了一个卡通猴子头像的模型,在onRender方法中设置模型的旋转角度:
@Override
protected void onRender(long ellapsedRealtime, double deltaTime) {
super.onRender(ellapsedRealtime, deltaTime);
mMonkey.setRotation(mAccValues.x, mAccValues.y, mAccValues.z);
}
此时我们想仿照这个例子,让我们的人脸模型和眼镜模型旋转起来。
mObamaFace.setRotation(mAccValues.x, mAccValues.y, mAccValues.z);
mGlasses.setRotation(mAccValues.x, mAccValues.y, mAccValues.z);
如果不止2个模型,每个模型都setRotation就太麻烦了。
我们可以对这些模型进行“编组”,用一个容器把它们装起来,统一进行旋转。
// 人脸模型obamaFace和眼镜模型glasses的初始化跟上面一样
// 创建一个空的Object3D,作为容器
mContainer = new Object3D();
// 把人脸模型作为Child添加到mContainer中
mContainer.addChild(obamaFace);
// 把眼镜模型作为Child添加到mContainer中
mContainer.addChild(glasses);
// 这次只把mContainer添加到场景中就可以了
getCurrentScene().addChild(mContainer);
然后在onRender方法中,对mContainer设置旋转角度就可以了:
mContainer.setRotation(mAccValues.x, mAccValues.y, mAccValues.z);
对模型旋转的例子稍加修改,就可以利用加速度计控制模型的平移:
@Override
protected void onRender(long ellapsedRealtime, double deltaTime) {
super.onRender(ellapsedRealtime, deltaTime);
mContainer.setPosition(mAccValues.x, mAccValues.y, mAccValues.z);
}
知道如何调整模型的参数、如何控制模型的旋转和平移,那么再结合人脸检测技术,就可以实现往人脸上加3D装饰品的效果。但这只适合于静态显示的模型。如果你希望显示的模型能够动态变化,比如检测到人脸张开嘴的时候,面具模型的嘴巴也能跟着张开,就需要根据人脸关键点,动态修改面具模型对应顶点的坐标。
面具模型有点复杂,这里用一个简单的正方形平面来代替,原理都是一样的。
先定义一个Geometry3D:
private Geometry3D mGeometry3D;
然后在Rajawali场景中加载一个平面Plane:
@Override
protected void initScene() {
// 创建一个宽高及宽高分段均为1的平面
Object3D plane = new Plane(1, 1, 1, 1);
// 之前的人脸模型和眼镜模型,其模型文件中已经定义了材质,所以不需要setMaterial
// Plane是Rajawali内置的几何体模型,本身不带有材质,如果不设置材质的话会报错:
// This object can't render because there's no material attached to it.
// 这里简单地给它一个红色材质
Material material = new Material();
material.setColor(Color.RED);
plane.setMaterial(material);
// 这个Geometry3D包含了模型的顶点坐标信息
mGeometry3D = plane.getGeometry();
getCurrentScene().addChild(plane);
}
然后我们在onRender方法里面把Plane的顶点坐标打印出来看看:
@Override
protected void onRender(long ellapsedRealtime, double deltaTime) {
// 获取顶点坐标
FloatBuffer vertBuffer = mGeometry3D.getVertices();
for (int i=0; i// Buffer里面是按照x0, y0, z0, x1, y1, z1...这样的顺序排列的
String type;
int tmp = i%3;
if (tmp == 0) {
type = "x";
} else if (tmp == 1) {
type = "y";
} else {
type = "z";
}
Log.i(TAG, "No." + i/3 + " " + type + " : " + vertBuffer.get(i));
}
}
打印结果如下:
No.0 x : -0.5
No.0 y : -0.5
No.0 z : 0.0
No.1 x : -0.5
No.1 y : 0.5
No.1 z : 0.0
No.2 x : 0.5
No.2 y : -0.5
No.2 z : 0.0
No.3 x : 0.5
No.3 y : 0.5
No.3 z : 0.0
那么这4个顶点的位置关系就是:
^ y
(-0.5, 0.5, 0.0) | (0.5, 0.5, 0.0)
1------------|-----------3
| | |
| | |
---------------------O-------------------> x
| | |
| | |
0------------|-----------2
(-0.5, -0.5, 0.0) | (0.5, -0.5, 0.0)
这里我们尝试改变一下Plane的左上角第1个点的y坐标,让它沿y轴变化,变化范围是-0.5~0.5。
修改onRender方法:
@Override
protected void onRender(long ellapsedRealtime, double deltaTime) {
// ellapsedRealtime是渲染持续的总时间
long time = ellapsedRealtime / 10000000;
// 让amp根据正弦曲线变化,取值范围为-0.5~0.5
double amp = 0.5 * Math.sin(Math.PI * time / 180.0);
FloatBuffer vertBuffer = mGeometry3D.getVertices();
// 按照x0, y0, z0, x1, y1, z1...这样的顺序排列
// 修改y1,即第4个数据,赋值为amp
vertBuffer.put(4, (float) (amp));
mGeometry3D.changeBufferData(mGeometry3D.getVertexBufferInfo(), vertBuffer, 0, vertBuffer.limit());
}