Alexey Podolian
Alexey Podolian

Reputation: 313

How to draw line/polyline on the plane using ARCore

In the Java ARCore Hello AR sample we can place Android object on the plane by tapping on the screen, how could we use this HitResult `s information to draw line between these objects?

Thanks for helping!

Upvotes: 5

Views: 6519

Answers (5)

Codemaker2015
Codemaker2015

Reputation: 15618

using System;

namespace GoogleARCore.Examples.HelloAR
{
    using System.Collections.Generic;
    using GoogleARCore;
    using GoogleARCore.Examples.Common;
    using UnityEngine;

    #if UNITY_EDITOR
    // Set up touch input propagation while using Instant Preview in the editor.
    using Input = InstantPreviewInput;
    #endif

    /// <summary>
    /// Controls the HelloAR example.
    /// </summary>
    public class HelloARController : MonoBehaviour
    {
        /// <summary>
        /// The first-person camera being used to render the passthrough camera image (i.e. AR background).
        /// </summary>
        public Camera FirstPersonCamera;

        /// <summary>
        /// A prefab for tracking and visualizing detected planes.
        /// </summary>
        public GameObject DetectedPlanePrefab;

        /// <summary>
        /// A model to place when a raycast from a user touch hits a plane.
        /// </summary>
        public GameObject AndyPlanePrefab;

        /// <summary>
        /// A model to place when a raycast from a user touch hits a feature point.
        /// </summary>
        public GameObject AndyPointPrefab;

        /// <summary>
        /// A gameobject parenting UI for displaying the "searching for planes" snackbar.
        /// </summary>
        public GameObject SearchingForPlaneUI;

        /// <summary>
        /// The rotation in degrees need to apply to model when the Andy model is placed.
        /// </summary>
        private const float k_ModelRotation = 180.0f;

        /// <summary>
        /// A list to hold all planes ARCore is tracking in the current frame. This object is used across
        /// the application to avoid per-frame allocations.
        /// </summary>
        private List<DetectedPlane> m_AllPlanes = new List<DetectedPlane> ();

        /// <summary>
        /// True if the app is in the process of quitting due to an ARCore connection error, otherwise false.
        /// </summary>
        private bool m_IsQuitting = false;

        private int count = 0;
        private Vector3[] pos = new Vector3[4];
        private LineRenderer lineRenderer;

        /// <summary>
        /// The Unity Update() method.
        /// </summary>
        public void Update ()
        {
            _UpdateApplicationLifecycle ();

            // Hide snackbar when currently tracking at least one plane.
            Session.GetTrackables<DetectedPlane> (m_AllPlanes);
            bool showSearchingUI = true;
            for (int i = 0; i < m_AllPlanes.Count; i++) {
                if (m_AllPlanes [i].TrackingState == TrackingState.Tracking) {
                    showSearchingUI = false;
                    break;
                }
            }

            SearchingForPlaneUI.SetActive (showSearchingUI);

            // If the player has not touched the screen, we are done with this update.
            Touch touch;
            if (Input.touchCount < 1 || (touch = Input.GetTouch (0)).phase != TouchPhase.Began) {
                return;
            }

            // Raycast against the location the player touched to search for planes.
            TrackableHit hit;
            TrackableHitFlags raycastFilter = TrackableHitFlags.PlaneWithinPolygon |
                                                       TrackableHitFlags.FeaturePointWithSurfaceNormal;

            if (Frame.Raycast (touch.position.x, touch.position.y, raycastFilter, out hit)) {
                // Use hit pose and camera pose to check if hittest is from the
                // back of the plane, if it is, no need to create the anchor.
                if ((hit.Trackable is DetectedPlane) &&
                                Vector3.Dot (FirstPersonCamera.transform.position - hit.Pose.position,
                                    hit.Pose.rotation * Vector3.up) < 0) {
                    Debug.Log ("Hit at back of the current DetectedPlane");
                } else {
                    // Choose the Andy model for the Trackable that got hit.
                    GameObject prefab;
                    if (hit.Trackable is FeaturePoint) {
                        prefab = AndyPointPrefab;
                    } else {
                        prefab = AndyPlanePrefab;
                    }

                    if (count < 2) {
                        pos [count] = hit.Pose.position;
                        // Instantiate Andy model at the hit pose.
                        var andyObject = Instantiate (prefab, hit.Pose.position, hit.Pose.rotation);

                        // Compensate for the hitPose rotation facing away from the raycast (i.e. camera).
                        andyObject.transform.Rotate (0, k_ModelRotation, 0, Space.Self);

                        // Create an anchor to allow ARCore to track the hitpoint as understanding of the physical
                        // world evolves.
                        var anchor = hit.Trackable.CreateAnchor (hit.Pose);

                        // Make Andy model a child of the anchor.
                        andyObject.transform.parent = anchor.transform;
                        count++;
                    }

//Drawing lines from the touched points using line renderer

                  if (count == 2) {

                        //Creating lineRenderer object
                      lineRenderer = new GameObject("Line").AddComponent<LineRenderer>();
                      lineRenderer.startColor = Color.black;
                      lineRenderer.endColor = Color.black;
                      lineRenderer.startWidth = 0.01f;
                      lineRenderer.endWidth = 0.01f;
                      lineRenderer.positionCount = 2;
                      lineRenderer.useWorldSpace = true;    

                      //Drawing line from first touched point to the second one
                      lineRenderer.SetPosition(0,pos[0]);
                      lineRenderer.SetPosition(1,pos[1]);

                  }
                }
            }
        }

        /// <summary>
        /// Check and update the application lifecycle.
        /// </summary>
        private void _UpdateApplicationLifecycle ()
        {
            // Exit the app when the 'back' button is pressed.
            if (Input.GetKey (KeyCode.Escape)) {
                Application.Quit ();
            }

            // Only allow the screen to sleep when not tracking.
            if (Session.Status != SessionStatus.Tracking) {
                const int lostTrackingSleepTimeout = 15;
                Screen.sleepTimeout = lostTrackingSleepTimeout;
            } else {
                Screen.sleepTimeout = SleepTimeout.NeverSleep;
            }

            if (m_IsQuitting) {
                return;
            }

            // Quit if ARCore was unable to connect and give Unity some time for the toast to appear.
            if (Session.Status == SessionStatus.ErrorPermissionNotGranted) {
                _ShowAndroidToastMessage ("Camera permission is needed to run this application.");
                m_IsQuitting = true;
                Invoke ("_DoQuit", 0.5f);
            } else if (Session.Status.IsError ()) {
                _ShowAndroidToastMessage ("ARCore encountered a problem connecting.  Please start the app again.");
                m_IsQuitting = true;
                Invoke ("_DoQuit", 0.5f);
            }
        }

        /// <summary>
        /// Actually quit the application.
        /// </summary>
        private void _DoQuit ()
        {
            Application.Quit ();
        }

        /// <summary>
        /// Show an Android toast message.
        /// </summary>
        /// <param name="message">Message string to show in the toast.</param>
        private void _ShowAndroidToastMessage (string message)
        {
            AndroidJavaClass unityPlayer = new AndroidJavaClass ("com.unity3d.player.UnityPlayer");
            AndroidJavaObject unityActivity = unityPlayer.GetStatic<AndroidJavaObject> ("currentActivity");

            if (unityActivity != null) {
                AndroidJavaClass toastClass = new AndroidJavaClass ("android.widget.Toast");
                unityActivity.Call ("runOnUiThread", new AndroidJavaRunnable (() => {
                    AndroidJavaObject toastObject = toastClass.CallStatic<AndroidJavaObject> ("makeText", unityActivity,
                                                                       message, 0);
                    toastObject.Call ("show");
                }));
            }
        }
    }
}

Upvotes: 0

Andrew G
Andrew G

Reputation: 689

The solution from @Arthulia does not work for me =(

But I have done a little bit different. It works:

in my onCreate:

ArFragment arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.vr_fragment);
arFragment.setOnTapArPlaneListener(
                (HitResult hitResult, Plane plane, MotionEvent motionEvent) -> {
                    addLineBetweenHits(hitResult, plane, motionEvent);
                });

and later:

private void addLineBetweenHits(HitResult hitResult, Plane plane, MotionEvent motionEvent) {
        Anchor anchor = hitResult.createAnchor();
        AnchorNode anchorNode = new AnchorNode(anchor);

        if (lastAnchorNode != null) {
            anchorNode.setParent(arFragment.getArSceneView().getScene());
            Vector3 point1, point2;
            point1 = lastAnchorNode.getWorldPosition();
            point2 = anchorNode.getWorldPosition();

        /*
            First, find the vector extending between the two points and define a look rotation
            in terms of this Vector.
        */
            final Vector3 difference = Vector3.subtract(point1, point2);
            final Vector3 directionFromTopToBottom = difference.normalized();
            final Quaternion rotationFromAToB =
                    Quaternion.lookRotation(directionFromTopToBottom, Vector3.up());
            MaterialFactory.makeOpaqueWithColor(getApplicationContext(), color)
                    .thenAccept(
                            material -> {
/* Then, create a rectangular prism, using ShapeFactory.makeCube() and use the difference vector
       to extend to the necessary length.  */
                                ModelRenderable model = ShapeFactory.makeCube(
                                        new Vector3(.01f, .01f, difference.length()),
                                        Vector3.zero(), material);
/* Last, set the world rotation of the node to the rotation calculated earlier and set the world position to
       the midpoint between the given points . */
                                Node node = new Node();
                                node.setParent(anchorNode);
                                node.setRenderable(model);
                                node.setWorldPosition(Vector3.add(point1, point2).scaled(.5f));
                                node.setWorldRotation(rotationFromAToB);
                            }
                    );
        lastAnchorNode = anchorNode;
    }

Upvotes: 1

Arthulia
Arthulia

Reputation: 190

In the section of code where you are grabbing the anchor to place the object, you should check if you already have a previous anchor. If you do have a previous anchor, grab the worldPosition (as a Vector3 object) from previous and current anchors, then calculate the difference between them, and create a line that length, and attach it to the scene at the halfway point between the two points. Finally, set the previousAnchor to the current one.

Here is some code that I used to solve this:

// Create the Anchor.
Anchor anchor = hitResult.createAnchor();
AnchorNode anchorNode = new AnchorNode(anchor);

// Code to insert object probably happens here

if (lastAnchorNode != null) {
    Vector3 point1, point2;
    point1 = lastAnchorNode.getWorldPosition();
    point2 = anchorNode.getWorldPosition();
    Node line = new Node();

    /* First, find the vector extending between the two points and define a look rotation in terms of this
        Vector. */

    final Vector3 difference = Vector3.subtract(point1, point2);
    final Vector3 directionFromTopToBottom = difference.normalized();
    final Quaternion rotationFromAToB =
          Quaternion.lookRotation(directionFromTopToBottom, Vector3.up());

    final Renderable[] lineRenderable = new Renderable[1];

    /* Then, create a rectangular prism, using ShapeFactory.makeCube() and use the difference vector
       to extend to the necessary length.  */

    MaterialFactory.makeOpaqueWithColor(this, color)
          .thenAccept(
                  material -> {
                      lineRenderable[0] = ShapeFactory.makeCube(new Vector3(.01f, .01f, difference.length()),
                              Vector3.zero(), material);
              });

    /* Last, set the world rotation of the node to the rotation calculated earlier and set the world position to
       the midpoint between the given points . */
    line.setParent(anchorNode);
    line.setRenderable(lineRenderable[0]);
    line.setWorldPosition(Vector3.add(point1, point2).scaled(.5f));
    line.setWorldRotation(rotationFromAToB);
}

lastAnchorNode = anchorNode;

Upvotes: 4

Fixus
Fixus

Reputation: 4641

Anchor will be helpful for you. thanks to it you can track tapped positions and use those coordinates between points. I've done something similar to count the distance between two points that I've tapped

Upvotes: 1

vvvifdks
vvvifdks

Reputation: 1

  1. I think that you should save all HitResults's information , especially for Pose‘s position.

  2. You should create a class as PlaneRenderer , so that it can draw a line between begin position and end position.

Upvotes: 0

Related Questions