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查看器,在导出后将能够看到模型在应用程序中的样子。

可视化模型

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。

导出资产

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

3、代码实现

3.1 配置AndroidManifest

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


<uses-permission android:name="android.permission.CAMERA" />

<uses-feature android:name="android.hardware.camera.ar" android:required="true" />

<application

...>

<meta-data android:name="com.google.ar.core" android:value="required" />

...

</application>

3.2 创建ARFragment

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


<fragment

    android:id="@+id/ux_fragment"

    android:name="com.google.ar.sceneform.ux.ArFragment"

...

/>

3.3 初次运行

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

权限请求

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

平面检测指示器

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

检测到平面

至此已经成功将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();

    }

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容