fbpx

Cоздаем AR-приложение с помощью Huawei AR Engine и Unity

Всем привет. В прошлой статье мы рассмотрели Huawei Scene Kit, позволяющий быстро интегрировать некоторые возможности Huawei AR Engine внутри вашего приложения. Это быстрый и удобный инструмент, но его функциональность сильно ограничено. 

Для создания сложных AR-приложений, позволяющих использовать все функции AR Engine нам понадобится что-то посерьезнее. Например, Unity, поскольку именно на нем создаются большинство коммерческих приложений и игр с дополненной реальностью. Сегодня мы расскажем, как можно создать AR-приложение с помощью Huawei AR Engine и Unity. Для этого нам понадобятся:

Unity 2017.4.4

AR Engine kit для Unity

Android SDK и Java Developer Kit (JDK)

Для начала стоит рассказать про версию Unity — на данный момент Huawei AR Engine умеет работать не со всеми версиями этого движка. Стопроцентная совместимость есть с версией 2017.4.4, в более новых версиях AR Engine может работать с ошибками, поскольку некоторые функции Unity, необходимые для работы AR Engine, в них изменены или попросту отсутствуют.

Для создания приложения у нас должна быть установлена нужная версия Unity (2017.4.4), а в настройках нужно указать пути к Android SDK и JDK:

Обратите внимание: Android SDK (входит в состав Android Studio) и Java Developer Kit должны быть самых новых версий.

Далее нам нужен плагин для Unity, добавляющий в него функционал Huawei AR Engine. Скачиваем Huawei AR Engine SDK. Внутри архива в папке Unity вы и найдете нужный плагин. 

Открываем Unity и создаем новый 3D-проект. Добавлем наш плагин через меню Assets -> Import Package -> Custom Package. В появившемся диалоговом окне выбираем «Импортировать все». 

Теперь в проект добавилось содержимое плагина – префабы, скрипты и прочее. Их можно найти в окне проекта в папке HuaweiARUnitySDK.

Создаем в сцене пустой объект, который станет контроллером нашей сцены. Назовем его Controller.

Добавим в сцену префаб, который можно найти в папке HuaweiARUnitySDK/Prefabs/ под названием PreviewCamera. «Родителем» этого префаба в иерархии должен быть Controller

PreviewCamera получает изображение с физической камеры устройства, а затем использует это изображение в качестве заднего фона для виртуальной камеры в сцене. 

Виртуальная камера при этом создается внутри скрипта BackGroundRenderer, который по умолчанию привязан к PreviewCamera

Вернемся к объекту Controller. Для него нужно создать два скрипта. Сначала скрипт под названием  SessionComponent:

namespace Common
{
    using HuaweiARUnitySDK;
    using UnityEngine;
    using HuaweiARInternal;

    public class SessionComponent : MonoBehaviour
    {
        [Tooltip("config")]
        public ARConfigBase Config;


        private bool isFirstConnect = true;//this is used to avoid multiple permission request when it was rejected
        private bool isSessionCreated =false;
        private bool isErrorHappendWhenInit = false;

        private string errorMessage = null;
        private void Awake()
        {
            Screen.sleepTimeout = SleepTimeout.NeverSleep;
        }

        private void Start()
        {
            //Init();
        }

        bool installRequested = false;
        void Init()
        {
            //If you do not want to switch engines, AREnginesSelector is useless.
            // You just need to use AREnginesApk.Instance.requestInstall() and the default engine
            // is Huawei AR Engine.
            AREnginesAvaliblity ability = AREnginesSelector.Instance.CheckDeviceExecuteAbility();
            if ((AREnginesAvaliblity.HUAWEI_AR_ENGINE & ability) != 0)
            {
                AREnginesSelector.Instance.SetAREngine(AREnginesType.HUAWEI_AR_ENGINE);
            }
            /*else if((AREnginesAvaliblity.GOOGLE_AR_CORE&ability) != 0)
            {
                AREnginesSelector.Instance.SetAREngine(AREnginesType.GOOGLE_AR_CORE);
            }*/
            else
            {
                errorMessage ="This device does not support AR Engine. Exit.";
                Invoke("_DoQuit", 0.5f);
                return;
            }

            try
            {
                switch (AREnginesApk.Instance.RequestInstall(!installRequested))
                {
                    case ARInstallStatus.INSTALL_REQUESTED:
                        installRequested = true;
                        return;
                    case ARInstallStatus.INSTALLED:
                        break;
                }

            }
            catch (ARUnavailableConnectServerTimeOutException e)
            {
                errorMessage ="Network is not available, retry later!";
                Invoke("_DoQuit", 0.5f);
                return;
            }
            catch (ARUnavailableDeviceNotCompatibleException e)
            {
                errorMessage ="This Device does not support AR!";
                Invoke("_DoQuit", 0.5f);
                return;
            }
            catch (ARUnavailableEmuiNotCompatibleException e)
            {
                errorMessage ="This EMUI does not support AR!";
                Invoke("_DoQuit", 0.5f);
                return;
            }
            catch (ARUnavailableUserDeclinedInstallationException e)
            {
                errorMessage ="User decline installation right now, quit";
                Invoke("_DoQuit", 0.5f);
                return;
            }
            if (isFirstConnect)
            {
                _Connect();
                isFirstConnect = false;
            }
        }
        public void Update()
        {
            _AppQuitOnEscape();
            AsyncTask.Update();
            ARSession.Update();
        }

        public void OnApplicationPause(bool isPaused)
        {
            if (isPaused)
            {
                ARSession.Pause();
            }
            else
            {
                if (!isSessionCreated)
                {
                    Init();
                }
                if (isErrorHappendWhenInit)
                {
                    return;
                }
                try
                {
                    ARSession.Resume();
                }
                catch (ARCameraPermissionDeniedException e)
                {
                    ARDebug.LogError("camera permission is denied");
                    errorMessage="This app require camera permission";
                    Invoke("_DoQuit", 0.5f);
                }
            }
        }

        public void OnApplicationQuit()
        {
            ARSession.Stop();
            isFirstConnect = true;
            isSessionCreated = false;
        }

        private void _Connect()
        {
            ARDebug.LogInfo("_connect begin");
            const string ANDROID_CAMERA_PERMISSION_NAME = "android.permission.CAMERA";
            if (AndroidPermissionsRequest.IsPermissionGranted(ANDROID_CAMERA_PERMISSION_NAME))
            {
                _ConnectToService();
                return;
            }
            var permissionsArray = new string[] { ANDROID_CAMERA_PERMISSION_NAME };
            AndroidPermissionsRequest.RequestPermission(permissionsArray).ThenAction((requestResult) =>
            {
                if (requestResult.IsAllGranted)
                {
                    _ConnectToService();
                }
                else
                {
                    ARDebug.LogError("connection failed because a needed permission was rejected.");
                    errorMessage="This app require camera permission";
                    Invoke("_DoQuit", 0.5f);
                    return;
                }
            });
        }
        private void _ConnectToService()
        {
            try
            {
                ARSession.CreateSession();
                isSessionCreated = true;
                ARSession.Config(Config);
                ARSession.Resume();
                ARSession.SetCameraTextureNameAuto();
                ARSession.SetDisplayGeometry(Screen.width, Screen.height);
            }
            catch (ARCameraPermissionDeniedException e)
            {
                isErrorHappendWhenInit = true;
                ARDebug.LogError("camera permission is denied");
                errorMessage="This app require camera permission";
                Invoke("_DoQuit", 0.5f);
            }
            catch (ARUnavailableDeviceNotCompatibleException e)
            {
                isErrorHappendWhenInit = true;
                errorMessage="This device does not support AR";
                Invoke("_DoQuit", 0.5f);
            }
            catch (ARUnavailableServiceApkTooOldException e)
            {
                isErrorHappendWhenInit = true;
                errorMessage="This AR Engine is too old, please update";
                Invoke("_DoQuit", 0.5f);
            }
            catch (ARUnavailableServiceNotInstalledException e)
            {
                isErrorHappendWhenInit = true;
                errorMessage="This app depend on AREngine.apk, please install it";
                Invoke("_DoQuit", 0.5f);
            }
            catch(ARUnSupportedConfigurationException e)
            {
                isErrorHappendWhenInit = true;
                errorMessage="This config is not supported on this device, exit now.";
                Invoke("_DoQuit", 0.5f);
            }
        }

        private void _AppQuitOnEscape()
        {
            if (Input.GetKey(KeyCode.Escape))
            {
                Invoke("_DoQuit", 0.5f);
            }
        }

        private void _DoQuit()
        {
            Application.Quit();
        }

        private void OnGUI()
        {
            GUIStyle bb = new GUIStyle();
            bb.normal.background = null;
            bb.normal.textColor = new Color(1, 0, 0);
            bb.fontSize = 45;

            GUI.Label(new Rect(0, Screen.height-100, 200, 200), errorMessage, bb);
        }
    }
}

Этот скрипт проверяет устройство на наличие установленного AR Engine и, если не находит, просит установить его, а если находит – создает объект класса ARSession. ARSession – это основной класс AR Engine, который получает информацию с сенсоров устройства и обрабатывает их. Впоследствии мы сможем обращаться к классу ARFrame, который содержит в себе всю информацию о том, что находится в кадре – плоскости, распознании лица, тела, какой-нибудь его конечности и т.д.

С ARFrame мы будем работать в следующем скрипте. Для работы этому скрипту нужен файл конфигурации, который подается в объект Config класса ARConfigBase. Создадим его в окне проекта:

Затем добавим этот файл в поле Config скрипта SessionComponent:

Второй скрипт, который нужно создать для Controller — TrackingController:

namespace Common
{
    using UnityEngine;
    using System.Collections.Generic;
    using HuaweiARUnitySDK;
    using System.Collections;
    using System;
    using Common;
    public class TrackingController : MonoBehaviour
    {
        [Tooltip("plane prefabs")]
        public GameObject planePrefabs;

        private List<ARPlane> newPlanes = new List<ARPlane>();

        public void Update()
        {
            _DrawPlane();
        }

        private void _DrawPlane()
        {
            newPlanes.Clear();
            ARFrame.GetTrackables<ARPlane>(newPlanes, ARTrackableQueryFilter.NEW);
            for (int i = 0; i < newPlanes.Count; i++)
            {
                GameObject planeObject = Instantiate(planePrefabs, Vector3.zero, Quaternion.identity, transform);
                planeObject.GetComponent<TrackedPlaneVisualizer>().Initialize(newPlanes[i]);
            }
        }
    }
}

На каждом кадре этот скрипт запускает метод DrawPlane(), в котором с помощью ARFrame.GetTrackables<ARPlane>(newPlanes, ARTrackableQueryFilter.NEW) мы получаем все объекты класса ARPlane, то есть распознанные в сцене плоскости, содержащиеся в кадре, а затем подаем эти плоскости в список newPlanes, причем только те, которые до этого еще найдены не были – за это отвечает фильтр ARTrackableQueryFilter.NEW.

Далее мы создаем объект planeObject из префаба planePrefabs, и обращаемся к его скрипту TrackedPlaneVisualizer, запуская метод Initialize из этого скрипта, подавая в качестве аргумента список с найденными плоскостями. Этот префаб мы еще в сцену не добавили, поэтому сделаем это сейчас.

Создайте в сцене префаб Plane. Это базовая плоскость, на которой впоследствии как на подложке будут выстраиваться найденные в кадре плоскости. Это сложный префаб и у него будут несколько компонентов: 

Во-первых скрипт TrackedPlaneVisualizer, отвечающий за визуализацию найденных плоскостей. Этот скрипт можно найти в папке HuaweiARUnitySDK/Scripts проекта. 

Во-вторых, добавьте компонет Mesh -> Mesh Filter и в поле Mesh выберите тип Plane. После этого добавьте компонент Mesh -> Mesh Renderer. Его настройки выставьте как на скриншоте. В качестве материала для рендерера создайте любой материал со стандартным шейдером и Rendering Mode: Fade. Специфика работы плагина такова, что ему необходима существующая в сцене плоскость, причем с видимым материалом, иначе найденные плоскости не будут отображаться. Чтобы во время работы приложения плоскость не была видна, просто поднимите ее на большое расстояние над камерой, поскольку ее нижняя грань является прозрачной. Альтернативно в скрипте TrackedPlaneVisualizer можно использовать следующую команду:

m_meshRenderer.material.color=new Color(0,0,0, 0.0f );

Эта команда выставит альфа-канал материала плоскости на 0, что сделает плоскость невидимой, но вместе с тем не будут видны и выстраиваемые на ней плоскости, найденные в AR Session. Поэтому данный метод рекомендуется использовать чтобы полностью скрыть от пользователя найденные плоскости. 

Созданный нами префаб Plane надо добавить в поле Plane Prefabs скрипта TrackingController объекта Controller:

 

Теперь у нас будут отображаться найденные плоскости. Но этого пока недостаточно, нужно еще разместить виртуальный объект на одной из этих плоскостей. Для этого создадим любой объект в сцене, который хотим визуализировать в дополненной реальности. 

Внутри скрипта TrackedPlaneVisualizer объявите этот объект:

private GameObject object;

Затем инициализируйте его:

Object = GameObject.Find("Object_name");

Где Object_name – имя вашего объекта в сцене.

Добавьте следующие строчки в метод Update():

if (Input.touchCount > 0) {
    List<ARHitResult> newPoints=ARFrame.HitTest(Screen.width/2,Screen.height/2);
    ARanchor anchor=newPoints[0].CreateAnchor();
    Pose pose=anchor.GetPose();
    object.transform.position = new Vector3(pose.position.x,pose.position.y,pose.position.z);
}

Вначале идет проверка, произошло ли касание экрана. Далее производится так называемый Hit Test. В виртуальном пространстве из камеры испускается виртуальный луч в направлении точки в реальном мире. Эта точка проецируется на экран устройства с координатами, заданными в параметрах HitTest(). В данном примере – ровно по центру экрана. Этот виртуальный луч проходит через все плоскости на своем пути, найденные в кадре, и записывает результаты пересечений ARHitResult в список newPoints. Результатов при этом может быть несколько, поскольку за одной плоскостью может быть другая, например поверхность стола и пол, на котором он стоит. 

Теперь возьмем самое первое пересечение, то есть то, на который пользователь непосредственно направил камеру при нажатии на экран, и создадим в этой точке якорь – объект класса ARanchor. Положение этого объекта в реальном мире относительно устройства будет запомнено и устройство постоянно будет знать, где находится этот якорь. 

Затем из якоря достанем позу с помощью GetPose(). Объект класса Pose содержит в себе все координаты и углы поворота. Осталось только модифицировать положение нашего объекта, используя эти координаты. 

Все готово! Осталось только собрать наш проект. Сначала идем в Edit -> Project Settings -> HuaweiAR и убеждаемся, что выставлен флаг Huawei AR Required. Затем идем в File -> Build Settings. 

В появившемся окне выбираем Android и Switch Platform. После этого нажимаем на кнопку Player Settings. Задаем названия приложения, компании, а также имя пакета. Minimum API и Target API выставляем на Android 9.0. Обязательно нужно выключить Multithread Rendering. После этого подключаем устройство и нажимаем Build&Run. 

После запуска приложения попробуйте подвигать камерой – на экране начнут визуализироваться плоскости, которые AR Engine увидел в реальном мире. После этого направьте камеру на одну из них и нажмите пальцем по экрану – на плоскости центре экрана должен возникнуть ваш объект.

Специально для этой статьи мы подготовили демо. Так как мы пока что ограничены размещением всего одного объекта в сцене, а показать что-то полезное на основе этого очень хочется, то я сделал простое спортивное приложение — спарринг-партнера по боксу, который позволяет вырабатывать реакцию и тренировать уклонения от ударов. Проект лежит на GitHub по этой ссылке.