这篇文章将分为以下几个部分:
1.启用Cloud Anchors API。
2.添加API密钥和清单权限。
3.导入Sceneform资产。
4.编写Cloud Anchors应用程序。
启用Cloud Anchors API
要从云托管和检索锚点,我们需要使用Cloud Anchors API。按照以下简单步骤启用API。
- 请访问:谷歌云服务官网并使用您的Google帐户登录或创建一个。
- 在Cloud Platform上创建项目并将其命名为您想要的任何名称。
- 创建项目后,单击屏幕左上角的三条水平线。
- 选择API和Serivces,然后单击“启用API和服务”。
- 搜索“Cloud Anchor”并显示ARCore Cloud Anchor API。您需要单击它并单击启用按钮。
- 然后从左侧的菜单中单击凭据。在这里,您将创建API凭据。
- 单击“创建凭据”,然后按照出现的简单步骤操作。最后,您将获得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()
}
}
网友评论