Google AR系列之Scenefrom进阶

298 阅读7分钟

前言

上一篇文章中我们认识了Google AR SDK——Scenefrom,并通过开发一个简单的demo了解了它的基本使用。本篇文章我们继续学习Scenefrom的一些其他功能。

移动

上一篇文章中我们只是将物体放到场景中了,下一步我们想让它可以拖动,其实很简单,使用TransformableNode即可,它是Node的一个子类,使用起来也很简单,如下:

val transformableNode = TransformableNode(transformationSystem)
transformableNode.setParent(anchorNode)
transformableNode.renderable = testModelRenderable

其中transformationSystem是ArFragment的一个属性。

这样我们放置物体后就可以拖动它,而且有近大远小,效果如下

521_1666149945.gif

动画

目前物体都是简单的放置在场景中,不够炫,下面我们来看看如果在场景中播放3D动画。

首先还是准备3D素材,可以用官方demo中animation项目中的素材: github.com/google-ar/s…

将andy_dance.fbx、andy_dance.sfa、andy_wave_l.fbx、andy_wave_r.fbx这四个文件拷贝到app/sampledata/models目录下

注意:这里不能放到其他目录下,因为文件中会在models目录下查找其他文件,如果放在其他目录下会导致倒入不成功。

在build.gradle中导入:

sceneform.asset('sampledata/models/andy_dance.fbx',
        'default',
        'sampledata/models/andy_dance.sfa',
        'src/main/res/raw/andy_dance',
        ['sampledata/models/andy_wave_r.fbx', 'sampledata/models/andy_wave_l.fbx'])

执行build自动在raw下创建sfb文件,这样就可以使用了,如下:

ModelRenderable.builder().setSource(activity, R.raw.andy_dance).build().thenAccept{
        renderable ->
    bodyRenderable = renderable
}


setOnTapArPlaneListener { hitResult, plane, motionEvent ->
    val anchor = hitResult.createAnchor()
    val anchorNode = AnchorNode(anchor)
    anchorNode.setParent(arSceneView.scene)

    val transformableNode = TransformableNode(transformationSystem)
    transformableNode.setParent(anchorNode)
    transformableNode.renderable = bodyRenderable
}

但是运行会发现它依然是静止不动的,因为还没有播放动画。

播放动画需要依赖:

implementation "com.google.ar.sceneform:animation:1.15.0"

先获取动画数据并创建动画:

val animData = bodyRenderable.getAnimationData("andy_dance")
modelAnimator = ModelAnimator(animData, bodyRenderable)

然后就可以通过ModelAnimator的函数来控制动画,比如开始播放

modelAnimator.start()

完整代码如下:

ModelRenderable.builder().setSource(activity, R.raw.andy_dance).build().thenAccept{
        renderable ->
    bodyRenderable = renderable

    val animData = bodyRenderable.getAnimationData("andy_dance")
    modelAnimator = ModelAnimator(animData, bodyRenderable)
}


setOnTapArPlaneListener { hitResult, plane, motionEvent ->
    val anchor = hitResult.createAnchor()
    val anchorNode = AnchorNode(anchor)
    anchorNode.setParent(arSceneView.scene)

    val transformableNode = TransformableNode(transformationSystem)
    transformableNode.setParent(anchorNode)
    transformableNode.renderable = bodyRenderable

    modelAnimator.start()
}

这样当我们放置后就可以看到机器人跳起舞来。如下:

573_1681438889.gif

骨骼

前面的3D素材都是单个的,下面来看看如何实现骨骼动画。

上面用到的素材其实就是一部分,只不过少了帽子,在demo中将帽子素材(baseball-cap.bin、baseball-cap.gltf、baseball-cap.sfa)拷贝过来,并导入:

sceneform.asset('sampledata/models/baseball-cap.gltf',
        'default',
        'sampledata/models/baseball-cap.sfa',
        'src/main/res/raw/baseball_cap')

然后需要修改一下上面的代码,不再使用TransformableNode,而用SkeletonNode替换,SkeletonNode也是Node的子类,用于处理骨骼动画。代码如下:

val bodyNode = SkeletonNode()
bodyNode.setParent(anchorNode)
bodyNode.renderable = bodyRenderable

然后创建一个Node,并将其连接到bodyNode的"hat_point",如下:

val boneNode = Node()
boneNode.setParent(bodyNode)
bodyNode.setBoneAttachment("hat_point", boneNode)

最后创建一个帽子的Node,并添加到boneNode上:

val hatNode = Node()
hatNode.renderable = hatRenderable
hatNode.setParent(boneNode)
hatNode.worldScale = Vector3.one()
hatNode.worldRotation = Quaternion.identity()

这里注意,需要为其设置worldScale和worldRotation,否则无法正常显示。

运行就会看到机器人带上了帽子,而且随着机器人跳舞帽子也进行跟随。

但是有一个小问题,机器人有两个触角,帽子是在触角之上的,这样有悬浮的感觉,所以还有微调一下帽子的位置,如下:

val pos = hatNode.worldPosition
// Lower the hat down over the antennae.
pos.y -= .1f
hatNode.worldPosition = pos

这样帽子就在正确的位置上了。效果如下:

572_1681438887.gif

交互

现在我们想与场景有一些交互,比如当我们点击机器人的帽子的时候可以讲帽子隐藏掉。其实这个很好来实现,为hatNode添加touch监听即可,如下:

hatNode.setOnTouchListener { hitTestResult, motionEvent ->
    hatNode.isEnabled = false
    return@setOnTouchListener true
}

这样当点击帽子的时候就可以将帽子隐藏。效果如下:

572_1681438887.gif

增强图像

关于AR我们见的最多的就是当拍到某个具体图片的时候,会出现相应的物体,比如支付宝的AR扫福。这其实是AR的增强图像功能。

要使用这个功能,就需要先将平面扫描关闭,因为不再需要扫描平面了,如下:

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    val view = super.onCreateView(inflater, container, savedInstanceState)
    //关闭平面检测的功能,这样才可以进行图片扫描
    planeDiscoveryController.hide()
    planeDiscoveryController.setInstructionView(null)
    arSceneView.planeRenderer.isEnabled = false

    return view
}

然后我们需要配置AugmentedImageDatabase,这里需要图片资源或者相关的数据库文件,在官方demo的augmentedimage项目( github.com/google-ar/s… )的assets目录下可以找到default.jpg和sample_database.imgdb。

这里有两种方式生成AugmentedImageDatabase,可以直接使用.imgdb文件来生成,也可以通过图片文件来生成,但是通过图片生成会相对慢一些。

重写ArFragment的getSessionConfiguration,在这里配置AugmentedImageDatabase,如下:

    override fun getSessionConfiguration(session: Session?): Config {
        val config =  super.getSessionConfiguration(session)
        context?.assets?.apply {
            //通过图片生成数据库,但是这样会比较慢
//            val bitmap = BitmapFactory.decodeStream(this.open("default.jpg"))
//            val augmentedImageDatabase = AugmentedImageDatabase(session)
//            augmentedImageDatabase.addImage("default.jpg", bitmap)

            val augmentedImageDatabase = AugmentedImageDatabase.deserialize(session, this.open("sample_database.imgdb"))
            config.augmentedImageDatabase = augmentedImageDatabase
        }
        return config
    }

这样需要增强的图片都设置完成了,当扫描到该图片的时候就会识别出来,那么识别出来后在哪里处理?

在场景SceneOnUpdateListener中来处理,通过arSceneView.scene.addOnUpdateListener来设置回调,代码如下:

arSceneView.scene.addOnUpdateListener { frameTime ->
    val frame = arSceneView.arFrame
    frame?.apply {
        val updateAugmentedImages = this.getUpdatedTrackables(AugmentedImage::class.java)
        for (augmentedImage in updateAugmentedImages){
            when(augmentedImage.trackingState){
                TrackingState.TRACKING -> {
                    if(!augmentedImageMap.containsKey(augmentedImage)){
                        val anchorNode = AnchorNode()
                        anchorNode.anchor = augmentedImage.createAnchor(augmentedImage.centerPose)
                        anchorNode.setParent(arSceneView.scene)

                        val node = Node()
                        node.setParent(anchorNode)
                        node.renderable = testModelRenderable
                        node.worldScale = Vector3(0.3f, 0.3f, 0.3f) //物体太大,缩小一点
                    }
                }
                TrackingState.STOPPED -> {
                    augmentedImageMap.remove(augmentedImage)
                }
            }
        }
    }
}

先通过arSceneView.arFrame来获取frame,通过frame可以得到扫描到的增强图片,是一个列表形式。

对于每个增强图片,如果是TRACKING状态,就可以在其上添加相应的物体。这里为了防止重复添加,通过一个Map来保存已添加的。

添加跟我们之前讲过的一样,先创建一个锚点节点AnchorNode,通过augmentedImage.createAnchor(augmentedImage.centerPose)可以得到增强图片的中心位置,以此为锚点。然后在AnchorNode上再添加展示的物体即可。

我们可以添加多个增强图片,这样可以通过AugmentedImagename来进行区分展示不同的物体。

上面代码的效果如下:

570_1681438882.gif

我们设定的是一个地球的图片,当扫描到这个图片就会在图片上添加一个Android机器人。

脸部增强

脸部增强与图片增强流程类似,首先也是关闭平面检测功能,如下:

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = super.onCreateView(inflater, container, savedInstanceState)
        //关闭平面检测的功能,这样才可以进行图片扫描
        planeDiscoveryController.hide()
        planeDiscoveryController.setInstructionView(null)

        return view
    }

然后需要开启脸部检测,先覆写ArFragment的getSessionFeatures函数,设置使用前置相机,如kotlin下:

    override fun getSessionFeatures(): Set<Session.Feature?>? {
        return EnumSet.of(Session.Feature.FRONT_CAMERA)
    }

然后覆写ArFragment的getSessionConfiguration函数进行相应设置,如下:

        override fun getSessionConfiguration(session: Session?): Config? {
            val config = Config(session)
            config.augmentedFaceMode = AugmentedFaceMode.MESH3D
            return config
        }

这样当我们打开这个页面的时候就会发现自动开启了前置摄像头并预览内容。

实际上这时候就可以开始检测脸部了,完成了第一步后下一步就是在检测到的脸上添加素材,在官方Demo(github.com/google-ar/s… )下的app/sampledata/models目录中有一个狐狸的素材,将这5个文件拷贝到自己项目的相同目录下,然后在build.gradle中添加如下代码:

        sceneform.asset('sampledata/models/fox_face.fbx',
                'sampledata/models/fox_face_material.mat',
                'sampledata/models/fox_face.sfa',
                'src/main/res/raw/fox_face')

这样build一次项目后就会在raw目录生成响应的sfb文件。

除了这个素材,在官方Demo中还提供了一个面具素材,在drawable-xxhdpi目录下,同样拷贝到自己项目中。

素材准备完成后就可以加载到程序中了,与之前一样,代码如下:

ModelRenderable.builder()
    .setSource(activity, R.raw.fox_face)
    .build()
    .thenAccept(
        Consumer { modelRenderable: ModelRenderable ->
            faceRegionsRenderable = modelRenderable
            modelRenderable.isShadowCaster = false
            modelRenderable.isShadowReceiver = false
        })

而面具素材只是一张图片,所以跟之前物体的加载不一样,要使用Texture,代码如下:

Texture.builder()
    .setSource(activity, R.drawable.fox_face_mesh_texture)
    .build()
    .thenAccept(Consumer { texture: Texture ->
        faceMeshTexture = texture
    })

下面就是获取脸部并进行增强了,与图像增强一样在在场景SceneOnUpdateListener回调中来得到检测结果,并进行处理,代码如下:

arSceneView.scene.addOnUpdateListener { frameTime ->
    val faceList: Collection<AugmentedFace> =
        arSceneView.session?.getAllTrackables<AugmentedFace>(
            AugmentedFace::class.java
        ) ?: listOf()

    //为每个面部添加素材
    for (face in faceList) {
        if (!faceNodeMap.containsKey(face)) {
            val faceNode = AugmentedFaceNode(face)
            faceNode.setParent(arSceneView.scene)
            faceNode.faceRegionsRenderable = faceRegionsRenderable
            faceNode.faceMeshTexture = faceMeshTexture
            faceNodeMap.put(face, faceNode)
        }
    }

    //当检测状态是STOPPED,则移除素材
    val iter: MutableIterator<Map.Entry<AugmentedFace, AugmentedFaceNode>> =
        faceNodeMap.entries.iterator()
    while (iter.hasNext()) {
        val (face, faceNode) = iter.next()
        if (face.trackingState == TrackingState.STOPPED) {
            faceNode.setParent(null)
            iter.remove()
        }
    }
}
  1. 首先通过Session的getAllTrackables函数获取所有检测到的面部数据,这是一个列表

  2. 然后为每个面部添加素材,同样为了防止重复添加,我们用一个map来进行记录。 这里可以看到使用的是AugmentedFaceNode,它可以设置faceRegionsRenderable和faceMeshTexture,对应的就是挂件和面具。

  3. 最后还要注意移除素材

这样就完成了脸部增强,运行效果如下:

569_1681438880.gif

总结

Google这套AR SDK即Scenefrom相对来说还是比较容易上手的,开发起来也比较简单,缺点是文档不够详细,很多细节需要我们自己来研究,这里也是简单的介绍一下各个功能和实现,如果想在项目中使用就需要深入的研究一下各个API。