如何使用重力在 Android 场景水平中定位 GLTF

How to orient GLTF in Android Sceneform level with gravity

我一直在努力尝试将垂直放置的 3d 模型 GLB 格式正确放置在垂直表面上。

Just to be clear, I am not referring to the difficulty of identifying vertical surface, that is a whole other problem in itself.

删除常见的设置样板以最小化此 post。

我正在使用扩展 ARFragment 的片段。

class SceneFormARFragment: ArFragment() {

当然,我对配置进行了一些调整。

override fun getSessionConfiguration(session: Session?): Config {
    val config = super.getSessionConfiguration(session)
    // By default we are not tracking and tracking is driven by startTracking()
    config.planeFindingMode = Config.PlaneFindingMode.DISABLED
    config.focusMode = Config.FocusMode.AUTO
    return config
}

为了开始和停止我的 AR 体验,我在片段中写了几个方法,如下所示。

private fun startTracking() = viewScope.launchWhenResumed {
    try {
        arSceneView.session?.apply {
            val changedConfig = config
            changedConfig.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
            configure(changedConfig)
        }

        logv("startTracking")
        planeDiscoveryController.show()
        arSceneView.planeRenderer.isVisible = true
        arSceneView.cameraStreamRenderPriority = 7
    } catch (ex: Exception) {
        loge("error starting ar session: ${ex.message}")
    }
}
private fun stopTracking() = viewScope.launchWhenResumed {
    try {
        arSceneView.session?.apply {
            val changedConfig = config
            changedConfig.planeFindingMode = Config.PlaneFindingMode.DISABLED
            configure(changedConfig)
        }

        logv("stopTracking")
        planeDiscoveryController.hide()
        arSceneView.planeRenderer.isVisible = false
        arSceneView.cameraStreamRenderPriority = 0
    } catch (ex: Exception) {
        loge("error stopping ar session: ${ex.message}")
    }
}

如果您想知道 "starting and stopping" AR 体验的原因是为了最大化此重叠屏幕上的其他 UX 交互的 GPU 周期,因此我们等待根据当前实时开始或停止正在发生的其他事情的数据状态。

好的,继续。

让我们回顾一下 HitResult 处理: 在这个方法中我做了一些事情:

  1. 从云端加载两种 TV 3d 模型(壁挂式和支架式)
  2. 如果他们点击了新区域,我会删除任何活动模型
  3. 根据命中结果创建一个锚节点并为其指定一个名称以便稍后将其删除
  4. 为其添加一个 TVTransformableNode 并为其指定一个名称以便稍后检索和操作它
  5. 确定水平支架安装 3D 模型电视的观看方向并将 anchorNode 的 worldRotation 设置为新的 lookRotation。 (注意*,我觉得旋转应该应用到 TVNode,但它似乎只在我出于某种原因将它应用到 AnchorNode 时才起作用。)这个相机位置数学似乎也有助于垂直壁挂式电视面向外和锚定正确。 (我查看了 GLB 模型,我知道它们从背面正确固定在墙壁模型上,从底部固定在地板模型上)
  6. 然后我将节点的平面移动限制为其各自的平面类型,这样地板模型就不会滑到墙上,墙壁模型也不会滑到地板上。

就是这样。水平放置效果很好,但垂直放置总是随机的。

OnTapArPlane 代码如下:

 private fun onARSurfaceTapped() {
    setOnTapArPlaneListener { hitResult, plane, _ ->
        var isHorizontal = false
        val renderable = when (plane.type) {
            Plane.Type.HORIZONTAL_UPWARD_FACING -> {
                isHorizontal = true
                standmountTVRenderable
            }
            Plane.Type.VERTICAL                 -> wallmountTVRenderable
            else                                -> {
                activity?.toast("Do you want it to fall on your head really?")
                return@setOnTapArPlaneListener
            }
        }

        lastSelectedPlaneOrientation = plane.type
        removeActive3DTVModel()

        val anchorNode = AnchorNode(hitResult.createAnchor())
        anchorNode.name = TV_ANCHOR_NAME
        anchorNode.setParent(arSceneView.scene)

        val tvNode = TransformableNode(this.transformationSystem)
        tvNode.scaleController.isEnabled = false
        tvNode.setParent(anchorNode)
        tvNode.name = TV_NODE_NAME
        tvNode.select()

        // Set orientation towards camera
        // Ref: https://github.com/google-ar/sceneform-android-sdk/issues/379
        val cameraPosition = arSceneView.scene.camera.worldPosition
        val tvPosition = anchorNode.worldPosition
        val direction = Vector3.subtract(cameraPosition, tvPosition)

        if(isHorizontal) {
            tvNode.translationController.allowedPlaneTypes.clear()
            tvNode.translationController.allowedPlaneTypes.add(Plane.Type.HORIZONTAL_UPWARD_FACING)
        } else {
            tvNode.translationController.allowedPlaneTypes.clear()
            tvNode.translationController.allowedPlaneTypes.add(Plane.Type.VERTICAL)
        }

        val lookRotation = Quaternion.lookRotation(direction, Vector3.up())
        anchorNode.worldRotation = lookRotation

        tvNode.renderable = renderable

        addVideoTo3DModel(renderable)
    }
}

Ignore the addvideoTo3dModel call, as that works fine, and I commented it out just to ensure it doesn't play a role.

我尝试过的东西。

  1. 像描述的那样提取不旋转的平移 here 有趣的是,它确实导致电视每次都与地板保持水平,但电视总是安装在底座上,而不是锚在底座上中后卫。所以不好。
  2. 我试过查看各种 post 并将 Unity 或 ARCore 的东西直接翻译成 Sceneform,但没有得到任何影响结果的东西。 example
  3. 我已经尝试从平面创建锚点和此 answer 中指示的姿势,但没有成功
  4. 我已经查看了这篇文章link,但从未发现任何有用的东西
  5. 我已经跟踪了这个 issue 并尝试了线程中人们推荐的解决方案,但没有成功
  6. 我试过的最后一件事,这有点尴尬哈哈。我在 Stack Overflow 中打开了所有带有 "SceneForm" 标记的 256 个,并检查了其中的每一个是否有任何帮助。

所以我已经用尽了互联网。我剩下的就是询问社区,当然也可以向 Android 的 SceneForm 团队发送帮助,我也将这样做。

My best guess is that I need to do the Quaternion.axisRotation(Vector3, Float), but everything I have guessed at or trialed and errored has not worked. I assume I need to set the localRotation using worldPostion values for xyz of the phone maybe to help identify gravity. I really just don't know anymore lol.

我知道 Sceneform 很新,文档HORRIBLE 并且由于缺少内容或文档 headers 也可能不存在。开发人员一定真的不希望人们使用它,但我猜 :(.

我要说的最后一件事是,在我当前的实现中,除了旋转的垂直放置外,一切都运行良好。只是为了避免在这个讨论中出现兔子痕迹,我没有任何其他问题。

哦,还有我注意到的最后一条线索。 电视几乎似乎围绕垂直平面的中心旋转,根据我点击的位置,底部几乎似乎指向平面的任意中心,如果这有助于任何人弄明白的话。

哦,是的,我知道 GLB 中缺少我的纹理,我将它们打包不正确并打算稍后修复它。

附上截图。

好吧,我终于明白了。花了一些时间和一些认真的尝试和错误来旋转每个节点、轴、角度和旋转,然后我终于把它放好。所以我会分享我的结果,以防其他人也需要它。

最终结果如下:

当然这对你如何握住 phone 以及它对周围环境的理解有轻微的主观性,但它现在总是非常接近水平,在我完成的横向和纵向测试中没有失败。

这就是我学到的东西。

在 anchorNode 上设置 worldRotation 将有助于使用一点减法使 3DModel 面向相机视图。

val cameraPosition = arSceneView.scene.camera.worldPosition
val tvPosition = anchorNode.worldPosition
val direction = Vector3.subtract(cameraPosition, tvPosition)
val lookRotation = Quaternion.lookRotation(direction, Vector3.up())
anchorNode.worldRotation = lookRotation

但是,这并没有解决垂直放置的方向问题。我发现如果我对外观旋转进行 90 度的 X 旋转,它每次都有效。它可能会根据您的 3d 模型而有所不同,但我的锚是中后卫,所以我不确定它如何确定向上的方向。但是,我注意到每当我在 tvNode 上设置 worldRotation 时,它都会放置电视水平,但会向前倾斜 90 度。所以玩转了各种轮换,终于找到答案了

val tvRotation = Quaternion.axisAngle(Vector3(1f, 0f, 0f), 90f)
tvNode.worldRotation = tvRotation

这解决了我的问题。所以 onSurfaceTap 和放置的最终结果是这样的:

   setOnTapArPlaneListener { hitResult, plane, _ ->
                var isHorizontal = false
                val renderable = when (plane.type) {
                    Plane.Type.HORIZONTAL_UPWARD_FACING -> {
                        isHorizontal = true
                        standmountTVRenderable
                    }
                    Plane.Type.VERTICAL -> wallmountTVRenderable
                    else -> {
                        activity?.toast("Do you want it to fall on your head really?")
                        return@setOnTapArPlaneListener
                    }
                }

                lastSelectedPlaneOrientation = plane.type
                removeActive3DTVModel()

                val anchorNode = AnchorNode(hitResult.createAnchor())
                anchorNode.name = TV_ANCHOR_NAME
                anchorNode.setParent(arSceneView.scene)

                val tvNode = TransformableNode(this.transformationSystem)
                tvNode.scaleController.isEnabled = false //disable scaling
                tvNode.setParent(anchorNode)
                tvNode.name = TV_NODE_NAME
                tvNode.select()

                val cameraPosition = arSceneView.scene.camera.worldPosition
                val tvPosition = anchorNode.worldPosition
                val direction = Vector3.subtract(cameraPosition, tvPosition)

                //restrict moving node to active surface orientation
                if (isHorizontal) {
                    tvNode.translationController.allowedPlaneTypes.clear()
                    tvNode.translationController.allowedPlaneTypes.add(Plane.Type.HORIZONTAL_UPWARD_FACING)
                } else {
                    tvNode.translationController.allowedPlaneTypes.clear()
                    tvNode.translationController.allowedPlaneTypes.add(Plane.Type.VERTICAL)

                    //x 90 degree rotation to flat mount TV vertical with gravity
                    val tvRotation = Quaternion.axisAngle(Vector3(1f, 0f, 0f), 90f)
                    tvNode.worldRotation = tvRotation
                }

                //set anchor nodes world rotation to face the camera view and up
                val lookRotation = Quaternion.lookRotation(direction, Vector3.up())
                anchorNode.worldRotation = lookRotation

                tvNode.renderable = renderable
                viewModel.updateStateTo(AriaMainViewModel.ARFlowState.REPOSITIONING)
            }

到目前为止,这已经过相当彻底的测试,在纵向和横向上没有问题。我还有其他关于 Sceneform 的问题,比如即使有有效的表面,点也只出现大约一半的时间,当然,如果墙上没有图片,当前的 SDK 无法在单色墙上进行垂直检测或者用来区分墙的东西。

执行屏幕截图也不是很好,因为它不包括 3D 模型,因此需要自定义像素复制工作,我的屏幕截图有点慢,但至少它们可以工作,不感谢 SDK。

所以他们还有很长的路要走,用他们的产品开辟道路是令人沮丧的,而且缺乏文档,而且肯定缺乏对客户服务的响应以及 GitHub 记录的问题,但嘿至少我明白了,我希望这对其他人有帮助。

编码愉快!