AR Camera开发记录(一) -- Rajawali的使用

转载:https://blog.csdn.net/torchlight2012/article/details/75722424

今年五一假期,心血来潮想研究Faceu的吐彩虹、3D装饰和换脸等功能是怎样实现的。 
期间进行了多次尝试,实现的效果都相差甚远。 
直到最近有幸拜读两位大神的博客专栏,得到了很大的启发。链接如下:

对于有相同兴趣的朋友,强烈建议先阅读上述大神的专栏。

在大神的博客的启发下,终于实现了稍微有一点接近的效果: 
这里写图片描述这里写图片描述

项目链接: https://github.com/SimonCherryGZ/ARCamera

如果觉得效果还算过得去,那么请继续往下看。

Rajawali的使用

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<vertBuffer.limit(); 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());
}

效果如下: 
这里写图片描述

猜你喜欢

转载自blog.csdn.net/niuba123456/article/details/81734644
今日推荐