Android ARCore – Distance from Camera

Filed Under: Android

In this tutorial, we’ll be discussing ARCore introduced by Google. We’ll be breezing through the basics of ARCore. After that, we’ll be developing an android application to get the distance of a node object from the camera. We have already covered a Hello World example in ARCore earlier. Now let’s get started!

ARCore Basics

Augmented Reality basically allows us to show virtual objects in a real-world environment and allow interactions with them.

ARCore requires :

  • Android N+
  • OpenGL 3.0 +

Knowledge in OpenGL 3d graphics programming is not necessary since Sceneform SDK is introduced for the same.

Sceneform allows us to create 3d models. It consists of ShapeFactory and MaterialFactory classes that allow us to create renderable objects from simple shapes and materials.
Shapes like a sphere, cube, cylinder can easily we created as we shall see later in this tutorial.

ARFragment is used to create an Ar scene and AR Session (both are created automatically).
Using an ARFragment we detect planes or feature points.

setOnTapArPlaneListener is set on the ArFragment to listen to changes whenever the click event takes place.

Few terms that form of the core of ARCore are listed below with descriptions:

  • Scene – This is where our 3D objects are rendered.
  • HitResult – On tap, this provides us the location of a real-world object. Basically, it estimates the position by finding the first point of intersection with an imaginary ray of light coming from infinity.
  • Anchor – A fixed location in real-world in terms of x,y,z coordinates in 3D space. Just like a ship anchor
  • Pose – provides the position and orientation of the object in the scene.
  • AnchorNode – This is the node that automatically positions itself in the world. This is the first node that gets set when a plane is detected.
  • TransformableNode – This node is where we set our 3d object. It can interact, scale, transform, rotate according to the user interactions.

In the next section, we’ll be developing our AR application in which we’ll calculate the distance of an object from us i.e the camera.

How is this done?
Once we place an object on a plane, we know it’s a pose.
As our scene changes, the frame gets updated. From the frame, we can get the camera pose.
Now that we have the two poses, the distance between them is just a Math formula!

Project Structure

Android Arcore Project Structure

Android Arcore Project Structure

Add the following dependency to the build.gradle:


implementation 'com.google.ar.sceneform.ux:sceneform-ux:1.10.0'

In the AndroidManifest file we need to add the following:


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

Inside the application tag add the following:


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

Code

The code for the activity_main.xml is given below:


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

    <fragment
        android:id="@+id/ux_fragment"
        android:name="com.google.ar.sceneform.ux.ArFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <TextView
        android:id="@+id/tvDistance"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom"
        android:background="@android:color/black"
        android:gravity="center"
        android:padding="8dp"
        android:text="Distance from camera"
        android:textColor="@android:color/white"
        android:textSize="20sp" />

</FrameLayout>

The code for the MainActivity.java is given below:


package com.journaldev.androidarcoredistancecamera;

import androidx.appcompat.app.AppCompatActivity;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;

import com.google.ar.core.Anchor;
import com.google.ar.core.Frame;
import com.google.ar.core.Pose;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.FrameTime;
import com.google.ar.sceneform.Scene;
import com.google.ar.sceneform.math.Vector3;
import com.google.ar.sceneform.rendering.Color;
import com.google.ar.sceneform.rendering.MaterialFactory;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.rendering.ShapeFactory;
import com.google.ar.sceneform.ux.ArFragment;
import com.google.ar.sceneform.ux.TransformableNode;

import java.util.Objects;

public class MainActivity extends AppCompatActivity implements Scene.OnUpdateListener {


    private static final double MIN_OPENGL_VERSION = 3.0;
    private static final String TAG = MainActivity.class.getSimpleName();

    private ArFragment arFragment;
    private AnchorNode currentAnchorNode;
    private TextView tvDistance;
    ModelRenderable cubeRenderable;
    private Anchor currentAnchor = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (!checkIsSupportedDeviceOrFinish(this)) {
            Toast.makeText(getApplicationContext(), "Device not supported", Toast.LENGTH_LONG).show();
        }

        setContentView(R.layout.activity_main);

        arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);
        tvDistance = findViewById(R.id.tvDistance);


        initModel();

        arFragment.setOnTapArPlaneListener((hitResult, plane, motionEvent) -> {
            if (cubeRenderable == null)
                return;

            // Creating Anchor.
            Anchor anchor = hitResult.createAnchor();
            AnchorNode anchorNode = new AnchorNode(anchor);
            anchorNode.setParent(arFragment.getArSceneView().getScene());

            clearAnchor();

            currentAnchor = anchor;
            currentAnchorNode = anchorNode;


            TransformableNode node = new TransformableNode(arFragment.getTransformationSystem());
            node.setRenderable(cubeRenderable);
            node.setParent(anchorNode);
            arFragment.getArSceneView().getScene().addOnUpdateListener(this);
            arFragment.getArSceneView().getScene().addChild(anchorNode);
            node.select();


        });


    }

    public boolean checkIsSupportedDeviceOrFinish(final Activity activity) {

        String openGlVersionString =
                ((ActivityManager) Objects.requireNonNull(activity.getSystemService(Context.ACTIVITY_SERVICE)))
                        .getDeviceConfigurationInfo()
                        .getGlEsVersion();
        if (Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) {
            Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later");
            Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG)
                    .show();
            activity.finish();
            return false;
        }
        return true;
    }

    private void initModel() {
        MaterialFactory.makeTransparentWithColor(this, new Color(android.graphics.Color.RED))
                .thenAccept(
                        material -> {
                            Vector3 vector3 = new Vector3(0.05f, 0.01f, 0.01f);
                            cubeRenderable = ShapeFactory.makeCube(vector3, Vector3.zero(), material);
                            cubeRenderable.setShadowCaster(false);
                            cubeRenderable.setShadowReceiver(false);
                        });
    }

    private void clearAnchor() {
        currentAnchor = null;


        if (currentAnchorNode != null) {
            arFragment.getArSceneView().getScene().removeChild(currentAnchorNode);
            currentAnchorNode.getAnchor().detach();
            currentAnchorNode.setParent(null);
            currentAnchorNode = null;
        }
    }

    @Override
    public void onUpdate(FrameTime frameTime) {
        Frame frame = arFragment.getArSceneView().getArFrame();

        Log.d("API123", "onUpdateframe... current anchor node " + (currentAnchorNode == null));


        if (currentAnchorNode != null) {
            Pose objectPose = currentAnchor.getPose();
            Pose cameraPose = frame.getCamera().getPose();

            float dx = objectPose.tx() - cameraPose.tx();
            float dy = objectPose.ty() - cameraPose.ty();
            float dz = objectPose.tz() - cameraPose.tz();

            ///Compute the straight-line distance.
            float distanceMeters = (float) Math.sqrt(dx * dx + dy * dy + dz * dz);
            tvDistance.setText("Distance from camera: " + distanceMeters + " metres");


            /*float[] distance_vector = currentAnchor.getPose().inverse()
                    .compose(cameraPose).getTranslation();
            float totalDistanceSquared = 0;
            for (int i = 0; i < 3; ++i)
                totalDistanceSquared += distance_vector[i] * distance_vector[i];*/
        }
    }
}

In the above code we do the following things:

  • Check if the phone is AR compatible.
  • Create a 3d cube shaped model.
  • Add it on tap once the plane is detected.
  • Update the distance from the camera to the anchor in every frame.
  • Once you tap again, clear the previous anchor.
We can set a tap listener on the transformable node as well node.setOnTapListener{} and do stuff with the node when it is clicked ( play sound etc.).

The output of the application in action is given below:

Android Arcore Distance Camera Output

Android Arcore Distance Camera Output

As you can see the distance changes when we move the camera closer to the cube we placed in the scene.

Distance between two nodes in ARCore is calculated in metres.

You can download the project from the link below.

You can also view the source code from our GitHub repository.

Leave a Reply

Your email address will not be published. Required fields are marked *

close
Generic selectors
Exact matches only
Search in title
Search in content
Search in posts
Search in pages