美文网首页
如何使用Google的Cloud Anchors API-(Ko

如何使用Google的Cloud Anchors API-(Ko

作者: 橙果子 | 来源:发表于2019-08-14 20:06 被阅读0次

这篇文章将分为以下几个部分:

1.启用Cloud Anchors API。
2.添加API密钥和清单权限。
3.导入Sceneform资产。
4.编写Cloud Anchors应用程序。

启用Cloud Anchors API

要从云托管和检索锚点,我们需要使用Cloud Anchors API。按照以下简单步骤启用API。

  1. 请访问:谷歌云服务官网并使用您的Google帐户登录或创建一个。
  2. 在Cloud Platform上创建项目并将其命名为您想要的任何名称。
  3. 创建项目后,单击屏幕左上角的三条水平线。
  4. 选择API和Serivces,然后单击“启用API和服务”。
  5. 搜索“Cloud Anchor”并显示ARCore Cloud Anchor API。您需要单击它并单击启用按钮。
  6. 然后从左侧的菜单中单击凭据。在这里,您将创建API凭据。
  7. 单击“创建凭据”,然后按照出现的简单步骤操作。最后,您将获得API密钥!

添加API密钥和清单权限

现在启用API密钥后,就可以启动Android Studio了。使用空活动创建新的Android Studio项目。

打开AndroidManifest.xml并在<application>标记之外添加以下权限:

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

现在,在<application>标记下直接添加以下元数据:

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

不要忘记将API密钥添加到上面的标记中。

要使用ARCore开发增强现实应用程序,您需要使用Sceneform插件和Sceneform SDK。下面的文章将向您展示如何为Android Studio启用Sceneform插件:

检查如何启用Sceneform插件

https://developers.google.com/ar/develop/java/sceneform/

启用插件后,将以下依赖项添加到应用程序级build.gradle文件中:

implementation 'com.google.ar:core:1.10.0' 
implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.10.0' 
implementation 'com.google.android.material:material:1.0.0'

现在我们准备将3D模型导入我们的Android应用程序。

导入Sceneform资产

我们将从poly.google.com下载3D资源,它是下载sceneform的.obj文件的绝佳工具。选择任何3D模型并以.obj格式下载。您将获得一个已下载到您的计算机上的zip文件。

右键单击Android Studio中的应用程序包,然后单击新建→包并将其命名为“ sampledata ”。解压缩此文件夹中的.zip文件。

解压缩文件后,找到.obj文件并右键单击它。选择“导入场景资源”并按照步骤完成在应用程序中导入3D模型。

编写Cloud Anchors应用程序

我们现在准备使用Sceneform SDK对增强现实应用程序进行编码。创建一个新类并从ArFragment扩展它。然后覆盖getSessionConfiguration()方法并添加以下代码:

class CloudAnchorFragment : ArFragment() {
    override fun getSessionConfiguration(session: Session?): Config {
        planeDiscoveryController.setInstructionView(null)
        val config: Config = super.getSessionConfiguration(session)
        config.cloudAnchorMode = Config.CloudAnchorMode.ENABLED
        return config
    }
}

使用planeDiscoveryController我们将instructionView设置为null。这将阻止任何教程阻止我们的视图。然后我们启用CloudAnchorsMode。为此,我们获取会话配置并将cloudAnchorsMode设置为Enabled。最后,我们返回自定义配置。

创建布局

我们需要将这个自定义ArFragment添加到Activity的布局文件中。我们还将添加两个额外的按钮。一个用于清除屏幕上的任何对象,另一个用于解析锚点。除此之外,我们将自定义ArFragment覆盖整个屏幕。
将以下代码添加到布局文件中:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <fragment
        android:id="@+id/ar_fragment"
        android:name="com.example.cloudanchors.CloudAnchorFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <Button
            android:id="@+id/btn_clear"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Clear" />
        <Button
            android:id="@+id/btn_resolve"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Resolve" />
    </LinearLayout>


</RelativeLayout>

有了这个,我们已经完成了我们的布局文件。我们准备在屏幕上渲染增强现实场景。

将3D模型添加到场景的方法

如果您已准备好以前的文章,您将知道我们使用2种方法将3D对象添加到增强现实场景中。第一种方法将.obj文件中的3D模型从Sceneform SDK加载到Renderable对象中。另一种方法实际上可以渲染到我们的场景。
因此,请将以下方法添加到MainAcitivity.java文件中

private fun cloudAnchor(newAnchor: Anchor?) {
    cloudAnchor?.detach()
    cloudAnchor = newAnchor
    appAnchorState = AppAnchorState.NONE
    snackbarHelper.hide(this)
}
private fun placeObject(fragment: ArFragment, anchor: Anchor, model: Uri) {
    ModelRenderable.Builder()
        .setSource(fragment.context, model)
        .build()
        .thenAccept { renderable ->
            addNodeToScene(fragment, anchor, renderable)
        }
        .exceptionally {
            val builder = AlertDialog.Builder(this)
            builder.setMessage(it.message).setTitle("Error!")
            val dialog = builder.create()
            dialog.show()
            return@exceptionally null
        }
}
private fun addNodeToScene(fragment: ArFragment, anchor: Anchor, renderable: ModelRenderable) {
    val node = AnchorNode(anchor)
    val transformableNode = TransformableNode(fragment.transformationSystem)
    transformableNode.renderable = renderable
    transformableNode.setParent(node)
    fragment.arSceneView.scene.addChild(node)
    transformableNode.select()
}

托管云锚

我们现在准备好将我们的锚点托管到云端。首先,我们将添加一个侦听器来检测平面上的触摸并将锚点+ 3D模型放置到场景中。

arFragment.setOnTapArPlaneListener { hitResult, plane, _ ->
    if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING || appAnchorState != AppAnchorState.NONE) {
        return@setOnTapArPlaneListener
    }
    val anchor = arFragment.arSceneView.session?.hostCloudAnchor(hitResult.createAnchor())


    placeObject(arFragment, cloudAnchor!!, Uri.parse("model.sfb"))
}

该hostCloudAnchors方法开始举办锚定到云的过程。它返回一个状态为TASK_IN_PROGRESS的Anchor。接下来,我们添加一个方法来设置全局锚点以引用托管锚点。它还将锚点的状态设置为NONE。

private fun cloudAnchor(newAnchor: Anchor?) {
    cloudAnchor?.detach()
    cloudAnchor = newAnchor
    appAnchorState = AppAnchorState.NONE
    snackbarHelper.hide(this)
}

为了保持Anchor的状态,我们需要创建一个Enum和一个全局变量来跟踪锚的当前状态。

class MainActivity : AppCompatActivity() {
    lateinit var arFragment: CloudAnchorFragment
    var cloudAnchor: Anchor? = null
    enum class AppAnchorState {
        NONE,
        HOSTING,
        HOSTED,
        RESOLVING,
        RESOLVED
    }
     ...

现在每当我们触摸场景时,我们都会开始主持锚点。因此,我们需要更新当前锚点,以及它的状态。在setOnTapArPlaneListener中添加以下代码

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    arFragment.setOnTapArPlaneListener { hitResult, plane, _ ->
        if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING || appAnchorState != AppAnchorState.NONE) {
            return@setOnTapArPlaneListener
        }
        val anchor = arFragment.arSceneView.session?.hostCloudAnchor(hitResult.createAnchor())
        cloudAnchor(anchor)
        appAnchorState = AppAnchorState.HOSTING
        snackbarHelper.showMessage(this, "Hosting anchor")
        placeObject(arFragment, cloudAnchor!!, Uri.parse("model.sfb"))
    }
}

接下来,我们需要定期检查锚是否已成功托管到云端。如果没有,那么我们需要通知用户失败。为此,我们将onUpdateListener添加到场景中。对于每个相机帧,此方法将检查是否托管锚。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    arFragment = supportFragmentManager.findFragmentById(R.id.ar_fragment) as CloudAnchorFragment
    arFragment.arSceneView.scene.addOnUpdateListener(this::onUpdateFrame)
    
    ....
}
fun onUpdateFrame(frameTime: FrameTime) {
    checkUpdatedAnchor()
}
@Synchronized
private fun checkUpdatedAnchor() {
    if (appAnchorState != AppAnchorState.HOSTING && appAnchorState != AppAnchorState.RESOLVING)
        return
    val cloudState: CloudAnchorState = cloudAnchor?.cloudAnchorState!!
    if (appAnchorState == AppAnchorState.HOSTING) {
        if (cloudState.isError) {
            snackbarHelper.showMessageWithDismiss(this, "Error hosting anchor...")
            appAnchorState = AppAnchorState.NONE
        } else if (cloudState == CloudAnchorState.SUCCESS) {
            val shortCode = storageManager.nextShortCode(this)
            storageManager.storeUsingShortCode(this, shortCode, cloudAnchor?.cloudAnchorId)
            snackbarHelper.showMessageWithDismiss(this, "Anchor hosted: $shortCode")
            appAnchorState = AppAnchorState.HOSTED
        }
    } else if (appAnchorState == AppAnchorState.RESOLVING) {
        if (cloudState.isError) {
            snackbarHelper.showMessageWithDismiss(this, "Error resolving anchor...")
            appAnchorState = AppAnchorState.NONE
        } else if (cloudState == CloudAnchorState.SUCCESS) {
            snackbarHelper.showMessageWithDismiss(this, "Anchor resolved...")
            appAnchorState = AppAnchorState.RESOLVED
        }
    }
}

这种方法可能看起来很长,但唯一的工作就是检查云锚HOSTING / RESOLVING的当前状态,并检查托管是成功还是失败。然后它相应地设置当前状态。
我们使用了一个StorageHelper类来在sharedPreferences中存储锚ID。您可以使用其他存储方法,例如Firebase,FireStore或您的自定义解决方案。但本文的目的是展示如何在云上存储增强现实锚点。因此,对于本地存储,我们将使用共享首选项。

/** Helper class for managing on-device storage of cloud anchor IDs. */
public class StorageManager {
    private static final String NEXT_SHORT_CODE = "next_short_code";
    private static final String KEY_PREFIX = "anchor;";
    private static final int INITIAL_SHORT_CODE = 142;
    /** Gets a new short code that can be used to store the anchor ID. */
    int nextShortCode(Activity activity) {
        SharedPreferences sharedPrefs = activity.getPreferences(Context.MODE_PRIVATE);
        int shortCode = sharedPrefs.getInt(NEXT_SHORT_CODE, INITIAL_SHORT_CODE);
        // Increment and update the value in sharedPrefs, so the next code retrieved will be unused.
        sharedPrefs.edit().putInt(NEXT_SHORT_CODE, shortCode + 1)
                .apply();
        return shortCode;
    }
    /** Stores the cloud anchor ID in the activity's SharedPrefernces. */
    void storeUsingShortCode(Activity activity, int shortCode, String cloudAnchorId) {
        SharedPreferences sharedPrefs = activity.getPreferences(Context.MODE_PRIVATE);
        sharedPrefs.edit().putString(KEY_PREFIX + shortCode, cloudAnchorId).apply();
    }
    /**
     * Retrieves the cloud anchor ID using a short code. Returns an empty string if a cloud anchor ID
     * was not stored for this short code.
     */
    String getCloudAnchorID(Activity activity, int shortCode) {
        SharedPreferences sharedPrefs = activity.getPreferences(Context.MODE_PRIVATE);
        return sharedPrefs.getString(KEY_PREFIX + shortCode, "");
    }
}

解析锚点

我们需要做的最后一件事是解决托管锚点。为此,我们将onClickListeners添加到两个按钮:Clear和Resolve。
对于clear按钮,我们只需调用cloudAnchors方法并将null传递给它。

btn_clear.setOnClickListener {
    cloudAnchor(null)
}

对于解析按钮,我们将在单击时显示一个对话框。它将有一个EditText用于输入锚的短代码。当用户提交短代码时,我们将解析锚并显示对象!
首先,我们需要一个dialogFragment。我为你创造了一个:

public class ResolveDialogFragment extends DialogFragment {
    interface OkListener {
        void onOkPressed(String dialogValue);
    }
    private OkListener okListener;
    private EditText shortCodeField;
    /** Sets a listener that is invoked when the OK button on this dialog is pressed. */
    void setOkListener(OkListener okListener) {
        this.okListener = okListener;
    }
    /**
     * Creates a simple layout for the dialog. This contains a single user-editable text field whose
     * input type is retricted to numbers only, for simplicity.
     */
    private LinearLayout getDialogLayout() {
        Context context = getContext();
        LinearLayout layout = new LinearLayout(context);
        shortCodeField = new EditText(context);
        shortCodeField.setInputType(InputType.TYPE_CLASS_NUMBER);
        shortCodeField.setLayoutParams(
                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        shortCodeField.setFilters(new InputFilter[]{new InputFilter.LengthFilter(8)});
        layout.addView(shortCodeField);
        layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        return layout;
    }

 @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
        builder
                .setView(getDialogLayout())
                .setTitle("Resolve Anchor")
                .setPositiveButton(
                        "OK",
                        (dialog, which) -> {
                            Editable shortCodeText = shortCodeField.getText();
                            if (okListener != null && shortCodeText != null && shortCodeText.length() > 0) {
                                // Invoke the callback with the current checked item.
                                okListener.onOkPressed(shortCodeText.toString());
                            }
                        })
                .setNegativeButton("Cancel", (dialog, which) -> {});
        return builder.create();
    }
}

将其添加到项目中,并在用户单击“解析”按钮时调用它,如下所示:

btn_resolve.setOnClickListener {
    if (cloudAnchor != null) {
        snackbarHelper.showMessageWithDismiss(this, "Please clear the anchor")
        return@setOnClickListener
    }
    val dialog = ResolveDialogFragment()
    dialog.setOkListener(this::onResolveOkPressed)
    dialog.show(supportFragmentManager, "Resolve")
}

这是onResolveOkMethod:

fun onResolveOkPressed(dialogVal: String) {
    val shortCode = dialogVal.toInt()
    val cloudAnchorId = storageManager.getCloudAnchorID(this, shortCode)
    val resolvedAnchor = arFragment.arSceneView.session?.resolveCloudAnchor(cloudAnchorId)
    cloudAnchor(resolvedAnchor)
    placeObject(arFragment, cloudAnchor!!, Uri.parse("model.sfb"))
    snackbarHelper.showMessage(this, "Now resolving anchor...")
    appAnchorState = AppAnchorState.RESOLVING
}

它得到了短代码并将对象解析为我们的增强现实场景。

请注意,我们也经常使用SnackBarHelper类。这是谷歌管理Android中零食栏的另一个类别。这是它的链接:SnackbarHelper

我们完成了!

这是您最终的MainActivity.kt类的样子:

class MainActivity : AppCompatActivity() {
    lateinit var arFragment: CloudAnchorFragment
    var cloudAnchor: Anchor? = null
    enum class AppAnchorState {
        NONE,
        HOSTING,
        HOSTED,
        RESOLVING,
        RESOLVED
    }
    var appAnchorState = AppAnchorState.NONE
    var snackbarHelper = SnackbarHelper()
    var storageManager = StorageManager()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        arFragment = supportFragmentManager.findFragmentById(R.id.ar_fragment) as CloudAnchorFragment
        arFragment.arSceneView.scene.addOnUpdateListener(this::onUpdateFrame)
        arFragment.planeDiscoveryController.hide()
        arFragment.planeDiscoveryController.setInstructionView(null)
        btn_clear.setOnClickListener {
            cloudAnchor(null)
        }
        btn_resolve.setOnClickListener {
            if (cloudAnchor != null) {
                snackbarHelper.showMessageWithDismiss(this, "Please clear the anchor")
                return@setOnClickListener
            }
            val dialog = ResolveDialogFragment()
            dialog.setOkListener(this::onResolveOkPressed)
            dialog.show(supportFragmentManager, "Resolve")
        }

 arFragment.setOnTapArPlaneListener { hitResult, plane, _ ->
            if (plane.type != Plane.Type.HORIZONTAL_UPWARD_FACING || appAnchorState != AppAnchorState.NONE) {
                return@setOnTapArPlaneListener
            }
            val anchor = arFragment.arSceneView.session?.hostCloudAnchor(hitResult.createAnchor())
            cloudAnchor(anchor)
            appAnchorState = AppAnchorState.HOSTING
            snackbarHelper.showMessage(this, "Hosting anchor")
            placeObject(arFragment, cloudAnchor!!, Uri.parse("model.sfb"))
        }
    }
    fun onResolveOkPressed(dialogVal: String) {
        val shortCode = dialogVal.toInt()
        val cloudAnchorId = storageManager.getCloudAnchorID(this, shortCode)
        val resolvedAnchor = arFragment.arSceneView.session?.resolveCloudAnchor(cloudAnchorId)
        cloudAnchor(resolvedAnchor)
        placeObject(arFragment, cloudAnchor!!, Uri.parse("model.sfb"))
        snackbarHelper.showMessage(this, "Now resolving anchor...")
        appAnchorState = AppAnchorState.RESOLVING
    }
    fun onUpdateFrame(frameTime: FrameTime) {
        checkUpdatedAnchor()
    }

@Synchronized
    private fun checkUpdatedAnchor() {
        if (appAnchorState != AppAnchorState.HOSTING && appAnchorState != AppAnchorState.RESOLVING)
            return
        val cloudState: CloudAnchorState = cloudAnchor?.cloudAnchorState!!
        if (appAnchorState == AppAnchorState.HOSTING) {
            if (cloudState.isError) {
                snackbarHelper.showMessageWithDismiss(this, "Error hosting anchor...")
                appAnchorState = AppAnchorState.NONE
            } else if (cloudState == CloudAnchorState.SUCCESS) {
                val shortCode = storageManager.nextShortCode(this)
                storageManager.storeUsingShortCode(this, shortCode, cloudAnchor?.cloudAnchorId)
                snackbarHelper.showMessageWithDismiss(this, "Anchor hosted: $shortCode")
                appAnchorState = AppAnchorState.HOSTED
            }
        } else if (appAnchorState == AppAnchorState.RESOLVING) {
            if (cloudState.isError) {
                snackbarHelper.showMessageWithDismiss(this, "Error resolving anchor...")
                appAnchorState = AppAnchorState.NONE
            } else if (cloudState == CloudAnchorState.SUCCESS) {
                snackbarHelper.showMessageWithDismiss(this, "Anchor resolved...")
                appAnchorState = AppAnchorState.RESOLVED
            }
        }
    }
private fun cloudAnchor(newAnchor: Anchor?) {
        cloudAnchor?.detach()
        cloudAnchor = newAnchor
        appAnchorState = AppAnchorState.NONE
        snackbarHelper.hide(this)
    }
    private fun placeObject(fragment: ArFragment, anchor: Anchor, model: Uri) {
        ModelRenderable.Builder()
            .setSource(fragment.context, model)
            .build()
            .thenAccept { renderable ->
                addNodeToScene(fragment, anchor, renderable)
            }
            .exceptionally {
                val builder = AlertDialog.Builder(this)
                builder.setMessage(it.message).setTitle("Error!")
                val dialog = builder.create()
                dialog.show()
                return@exceptionally null
            }
    }
    private fun addNodeToScene(fragment: ArFragment, anchor: Anchor, renderable: ModelRenderable) {
        val node = AnchorNode(anchor)
        val transformableNode = TransformableNode(fragment.transformationSystem)
        transformableNode.renderable = renderable
        transformableNode.setParent(node)
        fragment.arSceneView.scene.addChild(node)
        transformableNode.select()
    }
}

相关文章

网友评论

      本文标题:如何使用Google的Cloud Anchors API-(Ko

      本文链接:https://www.haomeiwen.com/subject/wlwnjctx.html