[Перевод] Создание вашего первого ARCore-приложения

Создание вашего первого ARCore-приложения

В предыдущей статье я уже объяснил, что такое ARCore и как он помогает разработчикам создавать удивительные приложения дополненной реальности без необходимости понимания математики и OpenGL.

Если вы ещё не читали её, я настоятельно рекомендую это сделать, прежде чем перейти к этой статье и начать разработку ARCore-приложений.


Начало работы

Чтобы начать разработку ARCore-приложений, сначала необходимо добавить поддержку ARCore в свой проект. Это очень просто, так как мы будем использовать Android Studio и Sceneform SDK. Есть две основные операции, которые благодаря Sceneform выполняются автоматически:


  1. Проверка наличия ARCore.
  2. Запрос на разрешение использования камеры.

Вам не нужно беспокоиться об этих двух шагах при создании ARCore-приложения с помощью Sceneform SDK. Вам просто нужно добавить Sceneform SDK в ваш проект.

Создайте новый проект Android Studio с пустой Activity.

Добавьте следующую зависимость в файл build.gradle на уровне проекта:

dependencies {
    classpath 'com.google.ar.sceneform:plugin:1.5.0'
}

А эту зависимость добавьте в файл build.gradle на уровня приложения:

implementation "com.google.ar.sceneform.ux:sceneform-ux:1.5.0"

Теперь синхронизируйте проект с Gradle-файлами и дождитесь окончания сборки. Таким образом, в проект будут добавлены Sceneform SDK и плагин Sceneform для Android Studio. Это позволит вам просматривать файлы с разрешением .sfb, которые представляют собой 3D-модели, которые будут рендериться в вашей камере, а также поможет вам импортировать, просматривать и создавать 3D-ресурсы.


Создание вашего первого ARCore-приложения

Теперь, когда настройка Android Studio завершена и SDK Sceneform установлен, мы можем начать создание нашего первого ARCore-приложения.

Во-первых, нужно добавить Sceneform-фрагмент в наш layout. Это так называемая сцена, где будут размещаться все наши 3D-модели. Фрагмент самостоятельно позаботится об инициализации камеры и обработке разрешений.

Перейдите к своему основному layout файлу. В моём случае это файл activity_main.xml. И добавьте туда Sceneform-фрагмент:




    


Я установил значения ширины и высоты match_parent, чтобы сцена занимала весь экран. Вы можете выбрать размеры в соответствии с вашими требованиями.


Проверка совместимости

Это всё, что нужно сделать в layout файле. Теперь переходим к Activity, в моём случае это MainActivity. Добавьте в Activity метод:

public static boolean checkIsSupportedDeviceOrFinish(final Activity activity) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
        Log.e(TAG, "Sceneform requires Android N or later");
        Toast.makeText(activity, "Sceneform requires Android N or later", Toast.LENGTH_LONG).show();
        activity.finish();
        return false;
    }
    String openGlVersionString =
            ((ActivityManager) 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;
}

Этот метод проверяет, поддерживает ли ваше устройство Sceneform SDK или нет. SDK требует Android API уровня 27 или выше и OpenGL ES версии 3.0 или выше. Если устройство не поддерживает эти два параметра, сцена не будет загружена, и ваше приложение отобразит пустой экран.

Однако вы по-прежнему можете реализовывать все другие функции своего приложения, для которых не требуется Sceneform SDK.

После проверки совместимости мы можем создать нашу 3D-модель и прикрепить её к сцене.


Добавление assets

Теперь нужно добавить в проект 3D-модели, которые будут отображаться на вашем экране. Вы можете создавать эти модели самостоятельно, если вы знакомы с процессом их создания. Или же вы можете зайти на Poly.

Там вы найдете огромный репозиторий 3D-ресурсов на выбор. Кроме того, они бесплатны для скачивания.

Poly

В Android Studio откройте папку своего приложения в панели слева. Вам нужна папка sampledata. Эта папка будет содержать все ваши 3D-модели. Внутри этой папки создайте папку с названием своей модели.

В архиве, который вы скачаете с Poly, вы, скорее всего, найдёте 3 файла:


  1. .mtl-файл
  2. .obj-файл
  3. .png-файл

Наиболее важным из этих трёх файлов является файл .obj. Это и есть ваша модель. Поместите все 3 файла в sampledata → «папка вашей модели».

Путь к 3D-модели

Теперь щёлкните правой кнопкой мыши на файле .obj. Первым вариантом будет Import Sceneform Asset. Нажмите на него, не меняйте настройки по умолчанию, просто нажмите Finish в следующем окне. После этого синхронизируйте проект с Gradle-файлами.

Импорт 3D-ресурса, который будет использован в вашем проекте, завершён. Далее давайте используем 3D-модель в нашем коде и включим его в сцену.


Создание модели

Добавьте следующий код в вашу Activity, а я объясню его построчно:

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

ArFragment arFragment;
ModelRenderable lampPostRenderable;

@Override
@SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (!checkIsSupportedDeviceOrFinish(this)) {
        return;
    }
    setContentView(R.layout.activity_main);
    arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);

    ModelRenderable.builder()
            .setSource(this, Uri.parse("LampPost.sfb"))
            .build()
            .thenAccept(renderable -> lampPostRenderable = renderable)
            .exceptionally(throwable -> {
                Toast toast =
                        Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG);
                toast.setGravity(Gravity.CENTER, 0, 0);
                toast.show();
                return null;
            });

}

Сначала мы находим arFragment, который мы ранее добавили в layout. Этот фрагмент отвечает за хранение и работу сцены. Вы можете представить его в виде контейнера для нашей сцены.

Далее мы используем класс ModelRenderable для построения нашей модели. С помощью метода setSource мы загружаем нашу модель из .sfb-файла, который был сгенерирован при импорте ресурсов. Метод thenAccept получает модель после её создания, и мы устанавливаем загруженную модель в нашу переменную lampPostRenderable.

Для обработки ошибок у нас есть метод exceptionally, который вызывается в случае возникновения исключения.

Всё это происходит асинхронно, поэтому вам не нужно беспокоиться о многопоточности.

Теперь, когда модель загружена и сохранена в переменной lampPostRenderable, мы добавим её в нашу сцену.


Добавление модели в сцену

В arFragment находится наша сцена, и он будет получать события пользовательских касаний. Поэтому нам нужно установить слушатель onTap для нашего фрагмента, чтобы обрабатывать касания и размещать объекты, там, где это потребуется. Добавьте следующий код в метод onCreate:

arFragment.setOnTapArPlaneListener(
        (HitResult hitresult, Plane plane, MotionEvent motionevent) -> {
            if (lampPostRenderable == null){
                return;
            }

            Anchor anchor = hitresult.createAnchor();
            AnchorNode anchorNode = new AnchorNode(anchor);
            anchorNode.setParent(arFragment.getArSceneView().getScene());

            TransformableNode lamp = new TransformableNode(arFragment.getTransformationSystem());
            lamp.setParent(anchorNode);
            lamp.setRenderable(lampPostRenderable);
            lamp.select();
        }
);

Мы устанавливаем слушатель onTapArPlaneListener для нашего AR-фрагмента. Далее используется синтаксис лямбда-выражений. Если вы с ним не знакомы, то ознакомьтесь с этим небольшим гайдом по этой теме.

Сначала мы создаем якорь из HitResult с помощью hitresult.createAnchor() и сохраняем его в объекте Anchor.

Затем создаём узел из этого якоря. Он будет называться AnchorNode и будет прикреплён к сцене при помощи метода setParent.

Далее мы создаём TransformableNode, который и будет являться нашей моделью, и привязываем его к нашему узлу. TransformableNode по-прежнему не имеет никакой информации об объекте, который он должен отобразить. Мы передадим ему этот объект с помощью метода setRenderable, который в качестве параметра принимает объект типа ModelRenderable (помните, мы получили такой объект и назвали его lampPostRenderable?). И, наконец, вызоваем метод lamp.select();

Ох! Слишком много терминологии. Не волнуйтесь, сейчас всё объясню:


  1. Сцена: это место, где будут отображаться все ваши 3D-объекты. Эта сцена размещена в AR-фрагменте, который мы добавили в layout.


  2. HitResult: это воображаемая линия (или луч), идущая из бесконечности, которая даёт точку пересечения себя с объектом реального мира.


  3. Якорь: это фиксированное местоположение и ориентация в реальном мире. Его можно понимать как координаты (x, y, z) в трехмерном пространстве. Поза — это положение и ориентация объекта на сцене. Она используется для преобразования локального координатного пространства объекта в реальное координатное пространство.


  4. Якорный узел: это узел, который автоматически позиционирует себя в реальном мире. Это первый узел, который устанавливается при обнаружении плоскости.


  5. TransformableNode: это узел, с которым можно взаимодействовать. Его можно перемещать, масштабировать, поворачивать и так далее. В этом примере мы можем масштабировать наш объект и вращать его. Отсюда и название Transformable.


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

По итогу ваша Activity должна выглядеть следующим образом:

package com.ayusch.arcorefirst;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.Toast;

import com.google.ar.core.Anchor;
import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.ux.ArFragment;
import com.google.ar.sceneform.ux.TransformableNode;

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

    ArFragment arFragment;
    ModelRenderable lampPostRenderable;

    @Override
    @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (!checkIsSupportedDeviceOrFinish(this)) {
            return;
        }
        setContentView(R.layout.activity_main);
        arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);

        ModelRenderable.builder()
                .setSource(this, Uri.parse("LampPost.sfb"))
                .build()
                .thenAccept(renderable -> lampPostRenderable = renderable)
                .exceptionally(throwable -> {
                    Toast toast =
                            Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG);
                    toast.setGravity(Gravity.CENTER, 0, 0);
                    toast.show();
                    return null;
                });

            arFragment.setOnTapArPlaneListener(
                    (HitResult hitresult, Plane plane, MotionEvent motionevent) -> {
                        if (lampPostRenderable == null){
                            return;
                        }

                        Anchor anchor = hitresult.createAnchor();
                        AnchorNode anchorNode = new AnchorNode(anchor);
                        anchorNode.setParent(arFragment.getArSceneView().getScene());

                        TransformableNode lamp = new TransformableNode(arFragment.getTransformationSystem());
                        lamp.setParent(anchorNode);
                        lamp.setRenderable(lampPostRenderable);
                        lamp.select();
                    }
            );

    }

    public static boolean checkIsSupportedDeviceOrFinish(final Activity activity) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
            Log.e(TAG, "Sceneform requires Android N or later");
            Toast.makeText(activity, "Sceneform requires Android N or later", Toast.LENGTH_LONG).show();
            activity.finish();
            return false;
        }
        String openGlVersionString =
                ((ActivityManager) 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;
    }
}

Поздравляю! Вы только что завершили создание своего первого ARCore-приложения. Начните добавлять в него объекты и вы увидите, как они начнут оживать в реальном мире.

Это был ваш первый взгляд на то, как создать простое ARCore-приложение с нуля в Android Studio. В следующем уроке я углублюсь в ARCore и добавлю больше функциональности в приложение.

© Habrahabr.ru