ARCore 使用Sceneform 创建ARApp

1、概述

在前面一篇文章 ARCore相关 里已经讨论过一些关于AR 和ARCore的概念了,并基于OpenGL ES实现了一个ARApp 的demo。OpenGL ES是内置在Android系统中的一个图像处理框架,这个框架相对来说学习曲线较陡,并且通过其来调用ARCore所要写的重复代码较多。官方也发现了这个问题,所以在2017年推出ARCore之后,在2018年的IO大会上正式推出了Sceneform这个全新的用于Java / Android开发人员的3D框架。这篇文章来简单讨论下如何使用Sceneform来构建ARApp。先来看一下最终效果:

ARApp

如上面的gif所示,点击ADDBALL按钮会在地面上出现一个球体,并且这个球体在自动的旋转。下面来详细讨论下这个是怎么实现出来的。

2、前期准备

2.1 获取3D模型

为了在屏幕上显示上面的球体,首先需要拥有这个球体的3D模型。一般来说每个3D模型通常称为资产,它所承载的纹理或皮肤通常称为材质。有许多免费的3D模型网站可供使用。官方提供了一个很好的地方叫做Poly,在那里可以得到漂亮的3D模型。上面的球体模型就是从那里下载来的 球体模型(要梯子)。Sceneform支持渲染OBJ,FBX,glTF格式的模型。当然如果有专门的3D模型师根据你的需求帮你量身打造3D模型那就更棒啦。

3D模型

2.2 在项目中配置Sceneform

要使用Sceneform需要在build.gradle的dependencies配置对其的依赖,到我写这篇文章时候其版本是1.5.0。


implementation"com.google.ar.sceneform.ux:sceneform-ux:1.5.0"

2.3 安装Sceneform工具插件

Android studio为我们提供了插件Google Sceneform Tools。安装步骤如下(这是OS X下的Android studio顺序,Windows下的类似):

① Android Studio- > Preferences- >Plugins

② 选择Browse repositories…- >搜索Google Sceneform Tools

③ 点击 Install

这个工具的作用如下:

① 以将所有支持的3D模型导出到我们的项目中。

② 为模型自动配置gradle。每当导出模型时,需要注入gradle中的几个配置。


sceneform.asset('sampledata/mosaicball.obj',

'default',

'sampledata/mosaicball.sfa',

'src/main/assets/mosaicball')

③ 可视化3D模型,这个工具将为提供了一个3D查看器,在导出后将能够看到模型在应用程序中的样子。

ARCore 使用Sceneform 创建ARApp_第1张图片
可视化模型

2.4 导出资产

有了heartform工具插件,就可以导出到项目中了。为了保存所有数据,可以在项目app层级下面新建一个sampledata文件夹将所有下载的内容(mosaicball.mtl和mosaicball.obj)复制到该文件夹下。现在在项目中有模型,现在需要将它们导出到Sceneform资产和二进制文件。这时候就要使用上面下载的插件了。右键单击mosaicball.obj - >Import Sceneform Asset

通过导出,该插件将创建2个文件mosaicball.sfa(SceneForm资产描述)和mosaicball.sfb(SceneForm二进制)。二进制文件将随APK一起打包进去,并保留在assets文件夹中。转换后的描述文件sfa将不会被打包进apk。但这个文件可以用来更改对象属性,通过修改它来修改sfb。

ARCore 使用Sceneform 创建ARApp_第2张图片
导出资产

如上图点击finish按钮后,gradle会自动构建资产,同时在sampledata文件夹会生成mosaicball.sfa,在assets文件夹会生成mosaicball.sfb。同时双击sfa或者sfb文件,Aandroid Studio 就会显示该模型。

3、代码实现

3.1 配置AndroidManifest

首先AR是显示与虚拟的结合,所以一定要获取相机权限,同时还需要添加一个关于AR的feature 来判断手机是否支持AR功能。同时要在中添加元数据。










...



3.2 创建ARFragment

ARFragment 为开发者封装了权限查询请求等操作,使用起来非常简单,只要直接在布局文件中插入fragment 标签,并且对其name属性设置为 "com.google.ar.sceneform.ux.ArFragment" 其他的就可以像一个正常的fragment一样去设置其大小位置等属性了。




3.3 初次运行

如果不出意外这时候可以试着运行一下程序,此时会在支持ARCore的手机中看到以下操作ArFragment处理权限,ARCore会话创建和平面发现UI。同时对于初次运行会弹出相机许可请求对话框。

ARCore 使用Sceneform 创建ARApp_第3张图片
权限请求

权限请求同意后,会看到检测平面的指示器。这时候需要左右前后移动一下手机,使其来检测具有一些“纹理”或图案的平面。这边说明一下对于纯白色的平面,就目前来讲检测效果并不理想。

ARCore 使用Sceneform 创建ARApp_第4张图片
平面检测指示器

当成功检测到平面就可以看到平面上散布着的白点,这些白点就是之前一篇文章里面说到的PointCloud。

ARCore 使用Sceneform 创建ARApp_第5张图片
检测到平面

至此已经成功将ARCore给运行起来了。

3.4 添加3D模型

前面的ArFragment已经检测到平面,这一步要在其上面放置之前获取到的3D模型。为了方便起见下面的代码实现了将3D对象添加到ArFragment中心,当然必须在中心位置有检测到平面即存在PointCloud。整个Activity代码如下所示:


public class MainActivityextends AppCompatActivity {

    ArFragment fragment;

    Button button;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        fragment = (ArFragment)getSupportFragmentManager().findFragmentById(R.id.ux_fragment);

        button = findViewById(R.id.button);

        button.setOnClickListener(v -> addObject(Uri.parse("mosaicball.sfb")));

    }

    private void addObject(Uri parse) {

        Frame frame =fragment.getArSceneView().getArFrame();

        android.graphics.Point point = getScreenCenter();

        if (frame !=null) {

            List hits = frame.hitTest(point.x, point.y);

           for (HitResult hit : hits) {

                Trackable trackable = hit.getTrackable();

                if (trackableinstanceof Plane && ((Plane)trackable).isPoseInPolygon(hit.getHitPose())) {

                    placeObject(fragment, hit.createAnchor(), parse);

                    break;

                }

           }

        }

    }

    private void placeObject( ArFragment fragment, Anchor createAnchor, Uri model) {

        ModelRenderable.builder()

        .setSource(fragment.getContext(), model)

        .build()

        .thenAccept(modelRenderable -> addNodeToScene(fragment,createAnchor, modelRenderable))

        .exceptionally(throwable -> {

            AlertDialog.Builder builder =new AlertDialog.Builder(MainActivity.this);

            builder.setMessage(throwable.getMessage())

            .setTitle("error!");

            AlertDialog dialog = builder.create();

            dialog.show();

            return null;

        }) ;

    }

private void addNodeToScene( ArFragment fragment, Anchor createAnchor, ModelRenderable renderable) {

    AnchorNode anchorNode =new AnchorNode(createAnchor);

    TransformableNode transformableNode =new TransformableNode(fragment.getTransformationSystem());

    transformableNode.setRenderable(renderable);

    transformableNode.setParent(anchorNode);

    fragment.getArSceneView().getScene().addChild(anchorNode);

    transformableNode.select();

    }

    private android.graphics.Point getScreenCenter() {

        View vw = findViewById(R.id.ux_fragment);

        return new Point(vw.getWidth() /2, vw.getHeight() /2);

    }

}

这里对有些关键的步骤在下面进行说明。

3.4.1 addObject()

传入模型的Uri,然后识别屏幕中心并对该特定点执行命中测试。如果它是可追踪的,也就是说命中了PointCloud上的点,那么就调用placeObject()来进行下一步操作。命中测试说白来就是测试从屏幕中发出的点是不是能击中PointCloud上的点。

3.4.2 placeObject()

这个方法里面其实只执行了一行语句。在这里调用了Sceneform框架提供给我们的API,其作用是用来异步加载3D模型,并提供一种类似RXJava形式的函数式编程思想来处理错误情况。如果加载模型成功就调用addToNode()。

3.4.3 addNodeToScene()

该方法接受3个参数:Renderable,Anchor和ArFragment。其中Renderable是加载成功的3D模型,Anchor就是前面被击中的PointCloud上的点,在这里由上面讲到的可知是ArFragment的中心位置。

这边简单说明一下节点(Node)的概念,其包含Sceneform渲染,包括其位置,方向和可渲染对象以及与其交互(包括其碰撞形状和事件侦听器)所需的所有信息。节点可以添加到其他节点,形成父子关系。当一个节点是另一个节点的子节点时,它会随着它的父节点移动,旋转和缩放。一个节点可以有多个子节点,但只有一个父节点,从而形成一个树状结构,这种结构称为场景图(the scene graph)

在该方法里面创建了两个节点AnchorNode和TransformableNode。其中AnchorNode用来使对象保持在特定的指示位置。而TransformableNode可以使用手势选择,移动,旋转和缩放的节点。

TransformableNode效果

为TransformableNode设置renderable也就是3D模型,然后将父节点指定为AnchorNode。这意味着将模型置于该锚点位置,并且它是可放大缩小的并且可以进行移动。但移动的时候其实是TransformableNode在移动而AnchorNode其实还是不动的。

3.5 3D模型动起来

到上面最基础的一个ARApp已经基本完成了,相比最前面演示的只差最后一步就是让它动起来。这边我用的是自定义了一个Node,并用属性动画来使其不停的转动。自定义Node代码如下:


public class RotatingNode extends Node {

    private ObjectAnimator rotationAnimation = null;

    private float degreesPerSecond = 90.0f;

    private float lastSpeedMultiplier = 1.0f;

    private Float speedMultiplier = 1.0f;

    private Long animationDuration =(long) ((1000 * 360) / (degreesPerSecond * speedMultiplier));

    // 重载方法节点激活时调用

    @Override

    public void onActivate() {

        super.onActivate();

        startAnimation();

    }

    // 重载方法,节点取消激活状态时调用

    @Override

    public void onDeactivate() {

        super.onDeactivate();

        stopAnimation();

    }

    // ARCore 每一处理帧都会调用一次

    @Override

    public void onUpdate(FrameTime frameTime) {

        super.onUpdate(frameTime);

        if (rotationAnimation == null) {

            return;

        }

        Float speedMultiplier = this.speedMultiplier;

        // 如果速度没变就继续以之前速度运行.

        if (lastSpeedMultiplier == speedMultiplier) {

            return;

        }

        if (speedMultiplier == 0.0f) {

            // 转速为0则停止旋转

            rotationAnimation.pause();

        } else {

            // 速度改变重新运行属性动画

            rotationAnimation.resume();

            float animatedFraction = rotationAnimation.getAnimatedFraction();

            rotationAnimation.setDuration(animationDuration);

            rotationAnimation.setCurrentFraction(animatedFraction);

        }

        lastSpeedMultiplier = speedMultiplier;

    }

    // 设置速度

    void setDegreesPerSecond( Float degreesPerSecond) {

        this.degreesPerSecond = degreesPerSecond;

    }

    // 启动动画

    private void startAnimation() {

        if (rotationAnimation != null) {

            return;

        }

        rotationAnimation = createAnimator();

        rotationAnimation.setTarget( this);

        rotationAnimation.setDuration(animationDuration);

        rotationAnimation.start();

    }

    // 停止动画

    private void stopAnimation() {

        if (rotationAnimation == null) {

            return;

        }

        rotationAnimation.cancel();

        rotationAnimation = null;

    }

    // 返回一个 ObjectAnimator 用来使节点旋转起来

    private ObjectAnimator createAnimator()  {

        // 节点的位置和角度信息设置通过Quaternion来设置

        // 创建4个Quaternion 来设置四个关键位置

        Quaternion orientation1 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 0f);

        Quaternion orientation2 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 120f);

        Quaternion orientation3 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 240f);

        Quaternion orientation4 = Quaternion.axisAngle(new Vector3(0.0f, 1.0f, 0.0f), 360f);

        ObjectAnimator rotationAnimation =new ObjectAnimator();

        rotationAnimation.setObjectValues(orientation1, orientation2, orientation3, orientation4);

        // 设置属性动画修改的属性为localRotation

        rotationAnimation.setPropertyName( "localRotation");

        // 使用Sceneform 框架提供的估值器 QuaternionEvaluator 作为属性动画估值器

        rotationAnimation.setEvaluator(new QuaternionEvaluator());

        //  设置动画重复无限次播放。

        rotationAnimation.setRepeatCount(ObjectAnimator.INFINITE);

        rotationAnimation.setRepeatMode( ObjectAnimator.RESTART);

        rotationAnimation.setInterpolator(new LinearInterpolator());

        rotationAnimation.setAutoCancel(true);

        return rotationAnimation;

    }

}

有了上面的自定义旋转类以后只要稍稍修改一下addNodeToScene()方法就可以来,如下所示:


private void addNodeToScene( ArFragment fragment, Anchor createAnchor, ModelRenderable renderable) {

        AnchorNode anchorNode =new AnchorNode(createAnchor);

        RotatingNode rotatingNode =new RotatingNode();

        TransformableNode transformableNode =new TransformableNode(fragment.getTransformationSystem());

//        transformableNode.setRenderable(renderable);

//        transformableNode.setParent(anchorNode);

        rotatingNode.setRenderable(renderable);

        rotatingNode.addChild(transformableNode);

        rotatingNode.setParent(anchorNode);

        fragment.getArSceneView().getScene().addChild(anchorNode);

        transformableNode.select();

    }

至此再运行程序就能看到一开始所展示的效果了。

你可能感兴趣的:(ARCore 使用Sceneform 创建ARApp)