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

本文分享了作者通过学习大神博客并结合Rajawali引擎,实现AR相机特效的过程。包括如何调整3D模型参数、控制模型旋转和平移,以及如何根据人脸关键点动态修改模型顶点坐标。

今年五一假期,心血来潮想研究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.youkuaiyun.com/junzia/article/details/54300202
http://blog.youkuaiyun.com/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());
}

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

<think>我们正在讨论如何使用Rajawali创建3D室内地图引擎。Rajawali个基于Android的3D引擎,因此我们需要围绕Android开发环境展开。 关键点: 1. 环境搭建:添加Rajawali依赖 2. 创建基本的3D场景 3. 加载室内地图数据(例如,平面图、3D模型) 4. 实现交互(如平移、缩放、旋转) 5. 添加标记点(POI)等 步骤分解: 1. 环境搭建 在Android项目的build.gradle中添加Rajawali依赖: ```groovy dependencies { implementation 'org.rajawali3d:rajawali:1.1.970' } ``` 2. 创建渲染器(Renderer) 创建个继承自RajawaliRenderer的类,用于设置场景、灯光、相机等。 3. 加载模型 Rajawali支持多种3D模型格式(如obj、stl等)。我们可以使用Loader类加载模型。 4. 创建平面图 如果室内地图是平面图,我们可以创建个平面,并在上面绘制房间、走廊等。可以使用纹理贴图或直接绘制几何图形。 5. 交互控制 使用Rajawali的触摸事件和相机控制,实现用户对场景的交互。 6. 添加标记点 在特定位置添加3D对象(如小球)作为标记点,并可以添加点击事件。 下面是个简单的示例代码框架: 步骤1:创建渲染器 ```java public class IndoorMapRenderer extends RajawaliRenderer { public IndoorMapRenderer(Context context) { super(context); } @Override protected void initScene() { // 设置相机 getCurrentCamera().setPosition(0, 0, 10); getCurrentCamera().setLookAt(0, 0, 0); // 添加灯光 DirectionalLight light = new DirectionalLight(1, 1, 1); light.setPosition(0, 0, 4); getCurrentScene().addLight(light); // 创建地面(个平面) Material material = new Material(); material.setColor(Color.BLUE); Plane plane = new Plane(10, 10, 1, 1); plane.setMaterial(material); plane.setPosition(0, 0, 0); getCurrentScene().addChild(plane); // 加载模型(假设有个房间模型) LoaderOBJ loader = new LoaderOBJ(this, R.raw.room_obj); try { loader.parse(); Object3D room = loader.getParsedObject(); getCurrentScene().addChild(room); } catch (ParsingException e) { e.printStackTrace(); } } } ``` 步骤2:在Activity中设置RajawaliSurfaceView ```java public class IndoorMapActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); RajawaliSurfaceView surface = new RajawaliSurfaceView(this); setContentView(surface); surface.setSurfaceRenderer(new IndoorMapRenderer(this)); } } ``` 步骤3:实现交互 可以使用Rajawali提供的TouchHandler,或者自定义手势监听。Rajawali内置了旋转、缩放等手势,可以通过设置相机控制器实现: ```java surface.setOnTouchListener(new TouchListener(surface) { @Override public void onTouchEvent(MotionEvent event) { // 处理触摸事件 } }); ``` 或者使用内置的相机控制器: ```java getCurrentCamera().setEnableRotation(true); // 允许旋转 getCurrentCamera().setEnableZoom(true); // 允许缩放 ``` 步骤4:添加标记点 在特定位置添加个球体作为标记点: ```java Sphere poi = new Sphere(0.2f, 12, 12); poi.setPosition(1, 1, 0.1f); // 在(1,1)位置上方点 poi.setMaterial(new Material(Color.RED)); getCurrentScene().addChild(poi); ``` 步骤5:处理标记点点击事件 需要实现射线拾取(Ray picking): ```java @Override public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) { // 不需要实现 } @Override public void onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { // 将触摸点转换为3D空间中的射线 Ray ray = getCurrentCamera().getRayFromScreenCoordinates(event.getX(), event.getY()); // 检测与场景中物体的碰撞 Object3D hitObject = getCurrentScene().getObjectAt(ray); if (hitObject != null) { // 处理点击事件 } } super.onTouchEvent(event); } ``` 注意事项: 1. 性能优化:室内地图可能包含大量模型,需要优化(如使用纹理合并、细节层次LOD等)。 2. 数据来源:室内地图数据可以来自CAD图纸、3D建模软件导出的模型或自己构建的网格。 3. 定位:可以结合室内定位技术(如蓝牙信标、WiFi指纹)实时显示位置。 相关问题: 1. Rajawali支持哪些3D模型格式? 2. 如何优化Rajawali的渲染性能? 3. 如何将2D平面图转换为3D模型用于Rajawali? 4. 如何在Rajawali中实现路径规划的可视化? 5. Rajawali与Google Maps在室内地图应用中有何异同?</think>### 使用 Rajawali 创建 3D 室内地图引擎 Rajawali个强大的 Android 3D 引擎,非常适合开发室内地图应用。以下是关键实现步骤和技术要点: #### 1. **环境搭建** ```groovy // build.gradle dependencies { implementation 'org.rajawali3d:rajawali:1.2.1970' } ``` #### 2. **核心架构设计** ```mermaid graph TD A[地图数据] --> B[数据解析器] B --> C[3D模型生成器] C --> D[Rajawali渲染器] D --> E[用户交互控制器] E --> F[UI界面] ``` #### 3. **地图数据加载** ```java // 加载OBJ格式平面图 LoaderOBJ loader = new LoaderOBJ(getResources(), textureManager, R.raw.floorplan); try { loader.parse(); Object3D mapModel = loader.getParsedObject(); getCurrentScene().addChild(mapModel); } catch (ParsingException e) { e.printStackTrace(); } ``` #### 4. **三维空间映射** 将2D坐标转换为3D场景: $$ \begin{pmatrix} x \\ y \\ 0 \end{pmatrix} \Rightarrow \begin{pmatrix} x \\ 0 \\ y \end{pmatrix} $$ ```java // 坐标转换方法 public Vector3 to3DCoordinates(float x, float y) { return new Vector3(x, 0, y); // Y轴作为高度 } ``` #### 5. **关键功能实现** **a. 楼层切换** ```java public void switchFloor(int floorLevel) { // 当前楼层下移 currentFloor.setY(-5f); // 新楼层上移 nextFloor = loadFloorModel(floorLevel); nextFloor.setY(5f); // 动画过渡 new TranslateAnimation3D(currentFloor, new Vector3(0, -10, 0)) .registerListener(new AnimationEndListener() { @Override public void onAnimationEnd(Animation animation) { getCurrentScene().removeChild(currentFloor); } }); } ``` **b. 路径规划** ```java // A*算法实现路径查找 public List<Vector3> findPath(Vector3 start, Vector3 end) { // 实现路径搜索算法... return pathPoints; } // 渲染路径 public void renderPath(List<Vector3> path) { for(int i=0; i<path.size()-1; i++) { Line3D pathSegment = new Line3D(path.get(i), path.get(i+1), 0.1f, 0xFF00FF00); getCurrentScene().addChild(pathSegment); } } ``` **c. POI标记** ```java public void addPOIMarker(Vector3 position, String info) { Sphere marker = new Sphere(0.3f, 12, 12); marker.setPosition(position); marker.setMaterial(new Material(Color.RED)); // 添加点击事件 marker.setPickable(true); marker.setOnClickListener(object -> { showInfoPopup(info); }); } ``` #### 6. **性能优化技巧** 1. **LOD(细节层次)**: ```java mapModel.setLODEnabled(true); mapModel.addLevelOfDetail(5.0f, simplifiedModel); // 距离5米切简化模型 ``` 2. **纹理压缩**: ```java Texture texture = new Texture("floor_texture", new CompressedTexture(ETC1Util.ETC1_RGB8)); ``` 3. **空间分区**: ```java Octree octree = new Octree(); octree.addObjects(sceneObjects); // 加速碰撞检测 ``` #### 7. **完整场景初始化示例** ```java public class IndoorMapRenderer extends RajawaliRenderer { @Override protected void initScene() { // 1. 相机设置 getCurrentCamera().setPosition(0, 5, 10); getCurrentCamera().setLookAt(0, 0, 0); // 2. 灯光设置 DirectionalLight keyLight = new DirectionalLight(-1, -0.5f, -1); keyLight.setPower(1.5f); // 3. 加载地图 Object3D floorMap = loadMapModel("building.obj"); floorMap.setScale(0.01f); // 缩放比例 // 4. 添加POI addPOIMarker(new Vector3(2, 0.5f, 3), "接待处"); addPOIMarker(new Vector3(-1, 0.5f, 4), "电梯"); } @Override public void onOffsetsChanged(float x, float y, float z, float w) { // 处理设备旋转 } } ``` #### 8. **交互控制实现** ```java // 在Activity中 surfaceView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { // 1. 处理平移 if(event.getPointerCount() == 1) { camera.setPosition(deltaX, 0, deltaZ); } // 2. 处理缩放 if(event.getPointerCount() == 2) { float scaleFactor = calculatePinchScale(event); camera.setFieldOfView(60 * scaleFactor); } // 3. 选择POI if(event.getAction() == MotionEvent.ACTION_UP) { Ray ray = camera.getRayFromScreenCoordinates(event.getX(), event.getY()); Object3D picked = scene.getObjectAt(ray); if(picked != null) { handleObjectSelection(picked); } } return true; } }); ``` ### 注意事项 1. **坐标系转换**:室内地图通常使用$ (x,y) $笛卡尔坐标系,需转换为Rajawali的$ (x,z) $平面系 2. **比例尺精度**:保持$ 1 \text{单位} = 1 \text{米} $的比例关系 3. **数据格式**:推荐使用GLTF 2.0格式模型(支持PBR材质) 4. **内存管理**:大型建筑需动态加载区域 $$ \text{内存占用} \propto \text{可见区域面积} \times \text{纹理分辨率} $$ ### 推荐扩展库 - **Tango**:室内定位支持[^1] - **ARCore**:增强现实集成 - **OSMDroid**:2D/3D混合地图 --- ### 相关问题 1. Rajawali 如何实现室内导航的最短路径算法? 2. 如何处理多层建筑的楼层切换动画? 3. Rajawali 与 Sceneform 在室内地图开发中有何优劣? 4. 如何优化大型室内场景的渲染性能? 5. 如何将 CAD 建筑图纸转换为 Rajawali 可用的 3D 模型? [^1]: 室内定位技术可结合蓝牙信标或WiFi指纹实现精准定位
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值