После успеха мобильной игры Pokemon Go интерес к приложениям дополненной реальности (AR) на мобильных устройствах неуклонно рос. И у Android, и у iOS есть свои собственные фреймворки AR, но с NativeScript и доступным плагином AR вы можете начать учиться создавать собственное кроссплатформенное приложение AR с более низкой кривой обучения. Этот пост покажет вам основы разработки простого приложения AR с использованием NativeScript Vue для размещения, анимации и взаимодействия с 3D-объектами.

Https://www.youtube.com/watch?v=8cMZOnv0M7Y

Установите пустое приложение NS Vue

Для начала давайте создадим новое приложение NativeScript Vue с именем ns6ar с помощью интерфейса командной строки (NativeScript v6.5.o для этой статьи). При появлении запроса выберите для начала шаблон Vue.js и пустой. После этого запустите диагностику интерфейса командной строки, чтобы убедиться, что в вашей среде NS нет проблем. После этого добавьте в пустое приложение NativeScript AR plugin (v1.1.0 для этого поста).

tns create ns6ar
cd ns6ar
tns doctor
tns plugin add nativescript-ar

Предостережения

Прежде чем приступить к созданию приложения дополненной реальности для Android и iOS, следует помнить о нескольких важных моментах. Вы не можете использовать AR в симуляторе iOS, поэтому вам понадобится настоящее устройство Apple, поддерживающее ARkit. Для устройств iOS это означает, что вам понадобятся модели iPhone SE / 6s и iPad Pro / 2017 или новее, работающие под управлением как минимум iOS 11. Если вы хотите поддерживать отслеживание лиц, вам также понадобится устройство iOS. с фронтальной камерой TrueDepth, которая в настоящее время доступна только на iPhone X / XS / XS Max / XR, iPad Pro (11 дюймов) и iPad Pro (12,9 дюйма, 3-го поколения). Этот плагин поддерживает только ARkit v1, поэтому более новые версии (которые также требуют абсолютно новейшего оборудования) и их функции недоступны в текущей версии плагина.

Для Android вы можете использовать AR в определенных конфигурациях симулятора. Чтобы использовать симулятор для разработки, войдите в диспетчер AVD (Android Virtual Device) в Android Studio и создайте новое виртуальное устройство, используя базовую конфигурацию Pixel 2 (которая также включает поддержку Play Store, поскольку требуется пакет Google Play Services для AR. ). На следующем экране выберите Oreo API 27 или более новую версию ОС, а на последнем экране откройте дополнительные настройки и убедитесь, что задняя камера настроена на использование VirtualScene, как показано ниже.

Наконец, перейдите на страницу выпусков Android ARcore APK и загрузите Google_Play_Services_for_AR_1.16.0_x86_for_emulator.apk. Запустите симулятор и после его загрузки выполните следующую команду, чтобы установить библиотеку.

adb install -r Google_Play_Services_for_AR_1.16.0_x86_for_emulator.apk

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

Наконец, при разработке и внесении изменений в код приложения AR убедитесь, что вы отключили горячую перезагрузку модуля, так как это обычно приводит к сбою плагина (используйте tns run <platform> --no-hmr).

Настройте представление AR

Давайте начнем с изменения пустого шаблона приложения, чтобы мы могли использовать плагин AR для отображения представления дополненной реальности с помощью камеры устройства. Сначала нам нужно зарегистрировать плагин, добавив следующую строку в app/app.js после объявлений импорта:

Vue.registerElement("AR", () => require("nativescript-ar").AR);

Мы изменим главную страницу приложения, изменив содержимое app/components/Home.vue на:

<template>
    <Page>
        <ActionBar title="NS6Vue AR"></ActionBar>
        <GridLayout>
            <AR trackingMode="WORLD" debugLevel="FEATURE_POINTS" :planeMaterial="planeMaterial" planeOpacity="0.4" planeDetection="HORIZONTAL" showStatistics="false" @arLoaded="arLoaded" @sceneTapped="sceneTapped" @planeTapped="planeTapped" @planeDetected="planeDetected">
            </AR>
        </GridLayout>
    </Page>
</template>
<script>
import { Color } from "tns-core-modules/color";
import { isIOS, isAndroid } from "tns-core-modules/platform";
export default {
    data() {
        return {
            planeMaterial: new Color("white"),          
        }
    },
    methods: {
        arLoaded(arLoadedEventData) {
            console.log(">> AR Loaded!");
            const arView = arLoadedEventData.object;
        },
        planeDetected(ARPlaneDetectedEventData) {
            console.log(">> plane Detected!")            
        },
        planeTapped(arPlaneTappedEventData) {
            console.log(">> plane Tapped!")
            const arPlane = arPlaneTappedEventData.object;
            const position = arPlaneTappedEventData.position  
            console.log(position)
        },
        sceneTapped(ARSceneTappedEventData) {
            console.log(">> scene Tapped!")
            const arScene = ARSceneTappedEventData.object;
            const position = ARSceneTappedEventData.position
            console.log(position)
        }
    },
};
</script>
<style scoped lang="scss"> 
</style>

Страница состоит только из панели действий и тега представления AR с некоторыми настроенными свойствами и обработчиками событий, а также соответствующими методами обработчика, определенными в разделе script. Вы должны прочитать об этих свойствах и событиях, просмотрев документацию плагина и различные демонстрации приложений AR, включенные в плагин.

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

Свойства просмотра AR

  1. trackingMode - отслеживание «МИР» является режимом по умолчанию и наиболее часто используется в приложениях дополненной реальности. Этот режим сканирует область, видимую камерой, для обнаружения плоских поверхностей (плоскостей), с которыми взаимодействуют объекты AR. В этой публикации мы будем использовать режим World Tracking, чтобы продемонстрировать большинство функций, поддерживаемых этим плагином на обеих платформах. Другой вариант - отслеживание «ЛИЦА», которое пытается найти лица в представлении AR. Хороший пример приложения можно найти в каталоге демонстрационных очков, включенном в плагин. Функция отслеживания «IMAGE» пытается найти заранее заданные 2D-изображения во время сканирования (например, шаблон шахматной доски) для взаимодействия, и пример можно найти в каталоге demo-pokemon.
  2. planeDetection - Сообщает плагину, должен ли он пытаться обнаружить плоские поверхности (плоскость) и тип поверхности (ГОРИЗОНТАЛЬНАЯ или ВЕРТИКАЛЬНАЯ). Обнаружение вертикальной плоскости доступно только для iOS, поэтому при создании кроссплатформенного приложения с помощью этого плагина вам следует разрабатывать приложения только на основе горизонтальных поверхностей.
  3. debugLevel (только iOS) - указывает, какая дополнительная визуальная информация, связанная с отладкой, будет отображаться плагином. «FEATURE_POINTS» будет наиболее полезным и покажет вам визуальное представление результатов сканирования, когда вы панорамируете камеру, пытаясь обнаружить поверхности для взаимодействия.
  4. showStatistics (только для iOS) - показывает некоторые статистические данные, такие как FPS, в нижней части окна AR, хотя, как правило, это не так полезно для начальной разработки, поэтому вы можете просто оставить его отключенным, если не имеете дело с более сложные сцены и модели.
  5. planeMaterial (только для iOS) - позволяет указать объект NS Color или ARMaterial для применения к самолетам, когда они обнаруживаются подключаемым модулем при использовании устройства iOS. Для устройств Android при панорамировании камеры в поисках самолета будет отображаться только белый узор в виде горошек по умолчанию. В приведенном выше коде вы увидите, что мы выбрали белый цвет NS для обнаруженных самолетов на устройствах iOS. Если вы не укажете цвет или материал, то для обнаруженных плоскостей ничего не будет отображаться, что визуально затрудняет разработку.
  6. planeOpacity (только для iOS) - указывает, насколько непрозрачным будет отображаться обнаруженный материал / цвет плоскости, где 0 - невидимый, а 1 - сплошной.

Если вы запустите приложение сейчас на устройстве iOS, переместите его по плоской области, и вы должны увидеть что-то похожее на снимок экрана выше. Характерные точки появятся при сканировании области для получения 3D-информации и исчезнут, когда вы будете держать камеру неподвижно. Когда будет обнаружена плоская область, она будет закрашена как полупрозрачная белая плоскость, чтобы дать вам представление о том, что плагин AR обнаружил во время сканирования.

Если вы запустите приложение на устройстве Android и перемещаете его по плоской области, вы должны увидеть белые точки в горошек, наложенные на обнаруженные плоскости, как показано на скриншоте ниже. Свойства материала и непрозрачности не влияют на то, что отображается.

AR Просмотр событий

  1. arLoaded - отправляется, когда представление AR загружено и готово к взаимодействию. Если вы используете более медленное устройство или у вас долго работает код инициализации для вашего приложения, используйте это событие, чтобы предотвратить запуск любых функций, связанных с AR, до тех пор, пока представление AR не будет готово.
  2. planeDetected - аналогично событию arLoaded, вы можете установить здесь флаг, чтобы предотвратить любое взаимодействие с видом до тех пор, пока плоскость не будет обнаружена, если вы будете использовать плоскость в качестве ориентира для объектов в вашей AR-сцене.
  3. planeTapped - это одно из двух основных событий , используемых для взаимодействия с представлением AR для большинства приложений. Если вы разрабатываете приложение, основанное на привязке объектов AR к плоскости, то это будет то, что вы будете использовать для добавления их в представление контролируемым образом. Например, вы можете добавить объект в текущее представление, чтобы он выглядел так, как будто он лежит поверх плоской области, обнаруженной в представлении, когда пользователь касается его.
  4. sceneTapped - это другое главное событие, используемое для взаимодействия с представлением AR. В тех случаях, когда вам все равно, нужна ли добавляемому объекту точка привязки, соответствующая плоской поверхности, это позволяет вам добавлять объект в любое место, где пользователь касается вида. Например, вы можете использовать это, если хотите добавить 3D-модель, плавающую в пустом пространстве, где пользователь касается экрана, например, с демонстрацией солнечной системы, включенной в приложение.

Размещение модели

Запустите приложение, перемещайте камеру по плоской области, пока не будет обнаружена плоскость, нажмите где-нибудь на плоскости, и вы увидите консольное сообщение, в котором вы узнаете координаты места, на которое вы нажали на этой плоскости. На приведенном выше рисунке показана система координат, используемая плагином AR, причем каждая единица в системе соответствует примерно 1 метру в реальной жизни. Плагин имеет встроенные 3D-модели сфер, коробок и трубок, а также некоторые другие 2D-конструкции, включая объекты Image, Video, UIView и Plane. Он также может загружать более сложные 3D-модели в форматах DAE, USDZ и GLB, хотя я пока не добился больших успехов с любыми 3D-моделями, кроме тех, которые включены в демонстрационные версии. Имея это в виду, давайте начнем с добавления сферы к обнаруженной плоскости, где она будет касаться.

Замените функцию planeTapped в app/components/Home.vue на:

        planeTapped(ARPlaneTappedEventData) {
            console.log(">> plane Tapped!")
            const arPlane = ARPlaneTappedEventData.object;
            const position = ARPlaneTappedEventData.position
            console.log(position)
            const sphereRadius = 0.1
            arPlane.addSphere({
                radius: sphereRadius,
                position: {
                    x: position.x,
                    y: position.y + sphereRadius,
                    z: position.z - 0.5
                },
                materials: [new Color("blue")],
                onLongPress: interaction => {
                    console.log("Sphere was longpressed");
                },
                onTap: interaction => {
                    console.log("Sphere was tapped at coordinates " + interaction.touchPosition.x + " x " + interaction.touchPosition.y);
                },
                segmentCount: 100,
                draggingEnabled: false, //Android only
                rotatingEnabled: false, //Android only
                scalingEnabled: false, //Android only
                mass: 0, //iOS only
            }).then(arNode => {
                console.log("Sphere successfully added");
            })
        },

Используя объект arPlane в качестве ссылки на сцену AR, мы вызываем функцию addSphere с несколькими параметрами и обработчиками событий, чтобы добавить модель сферы. Сфера имеет radius 0,1 метра. Его position установлен в координаты точки касания плоскости, за исключением того, что мы сдвигаем сферу вверх (ось y) на ее радиус, чтобы она лежала чуть выше плоскости, а не пересекала ее, и мы также устанавливаем ее немного дальше ( ось z) из текущего вида. Мы окрасим сферу в синий цвет, присвоив ей material с помощью Color. Каждая модель AR поддерживает два обработчика событий, onTap и onLongPress, когда кто-то нажимает или долго нажимает на модель AR, что мы будем использовать позже. segmentCount используется для определения уровня детализации для модели сферы, и вы должны установить его ниже для меньших сфер или в более сложных сценах, чтобы поддерживать высокую скорость рендеринга. Следующие три свойства, используемые в этом примере draggingEnabled, rotatingEnabled и scalingEnabled, применимы только на Android, поэтому их следует просто оставить отключенными, поскольку они не будут влиять на взаимодействие модели на устройствах iOS. Если вы запустите это на Android и установите для них true, вы можете перетаскивать сферу, ущипнуть, чтобы сжать, растянуть, чтобы увеличить, и повернуть ее с помощью двух пальцев. Атрибут mass предназначен только для iOS, и вы можете присвоить ему положительное значение, чтобы он «падал» вниз. Однако будьте осторожны, если самолета нет или он упадет за край самолета, он упадет из поля зрения.

Нанесение материалов на модели

Хотя цвета подходят для простых объектов и сцен, гораздо интереснее иметь возможность применять изображения в качестве материалов к объектам AR. Чтобы плагин AR мог использовать эти изображения, вам нужно поместить их в папку app\App_Resources для каждой платформы. Я создал простое изображение с желтым фоном и 7 красными звездами, похожее на Жемчуг дракона из популярного аниме-сериала. Для Android я поместил копию в app/App_Resources/Android/src/main/assets/SevenStar.fw.png. Для iOS я также добавил копию в app/App_Resources/iOS/SevenStar.fw.png. Теперь мы можем изменить присвоение materials на:

{
  diffuse: { contents: "SevenStar.fw.png", wrapMode: "Clamp" },
}

Если вы запустите приложение сейчас на Android, вы должны увидеть что-то похожее на следующее:

Выглядит лучше, но ориентация модели и материала не совсем соответствует нашему текущему виду. Чтобы отрегулировать это, мы можем добавить свойство rotation к функции addSphere.

rotation:isIOS ? { x: -20, y: 0, z: 0 } : { x: 0, y: 100, z: -20 },

На Android теперь должна появиться сфера, которая выглядит так:

А на iOS это должно выглядеть так:

Имейте в виду, что iOS и Android будут отображать объекты и материалы по-разному с точки зрения положения, ориентации, масштаба и поворота, поэтому вам нужно будет настроить код для каждой платформы. Существует также ряд других свойств материалов, которые можно применить, хотя они также зависят от платформы и не всегда работают должным образом. Вы заметите, что объекты Android будут иметь вид плоского матового материала и по умолчанию отбрасывать тень. Объекты iOS будут иметь вид глянцевого металлического материала и не будут отбрасывать тени. Вы можете просмотреть документацию и демонстрации плагинов, чтобы увидеть различные свойства материалов, а затем протестировать их на iOS и Android, чтобы увидеть, какие из них поддерживаются и работают в соответствии с AR SDK каждой платформы и связями исходного кода плагина. Вы также можете использовать собственный код для достижения лучших результатов в некоторых случаях.

Взаимодействие с моделью

Каждый объект AR поддерживает два обработчика событий, а также ряд вызовов функций, что можно увидеть в документации к плагину. »Используя два обработчика событий, которые мы вставили для нашей сферы, давайте воспользуемся некоторыми функциями преобразования, когда пользователь нажимает и удерживает сферу. Измените эти функции, чтобы они выглядели так:

                onLongPress: interaction => {
                    console.log("Sphere was longpressed");
                    interaction.node.remove()
                },
                onTap: interaction => {
                    console.log("Sphere was tapped at coordinates " + interaction.touchPosition.x + " x " + interaction.touchPosition.y);
                    const scale = 1
                    interaction.node.scaleBy(scale)
                    setTimeout(() => {
                        interaction.node.scaleBy(-scale)
                    },1000)
                },

Теперь, если вы долго нажимаете на сферу, она будет удалена со сцены. При нажатии на сферу она увеличится вдвое, а через секунду вернется к исходному размеру. Точно так же мы можем применить ряд других функций преобразования помимо этих двух, таких как moveTo, moveBy, scaleBy, rotateBy и setVisible.

Анимация моделей

Если вы протестируете приложение в его нынешнем виде, то увидите, что использование функции scaleBy вызовет резкое изменение масштаба, а не постепенное. Это можно сгладить двумя способами. Для iOS мы можем установить флаг сцены AR с if (isIOS) SCNTransaction.animationDuration = .5;, который будет интерполировать анимацию изменения масштаба, чтобы сгладить ее. Для Android вместо этого нам придется полагаться на использование таймера для аппроксимации шагов интерполяции. Пример использования множества маленьких шагов для анимации преобразования объекта можно увидеть в демонстрации солнечной системы.

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

               onTap: interaction => {
                    console.log("Sphere was tapped at coordinates " + interaction.touchPosition.x + " x " + interaction.touchPosition.y);
                    if (!interaction.node.animTimer) { //start the timer
                        let walkDegrees = 1;
                        const radius = 1;
                        const fps = 120;
                        interaction.node.animTimer = setInterval(() => {
                            walkDegrees += 1 / fps
                            let newX, newY, newZ
                            newX = interaction.node.position.x - Math.cos(walkDegrees) * radius
                            newY = interaction.node.position.y
                            newZ = interaction.node.position.z - Math.sin(walkDegrees) * radius
                            interaction.node.moveTo({ x: newX, y: newY, z: newZ });
                            interaction.node.rotateBy({
                                x: 0,
                                y: walkDegrees,
                                z: 0
                            });
                        },1000 / fps);
                    } else {
                        clearInterval(interaction.node.animTimer)
                        interaction.node.animTimer = null
                    }
                },

Если вы запустите код сейчас, нажмите на плоскость, чтобы добавить сферу, а затем нажмите на сферу, она начнет вращаться и двигаться по круговой траектории. Глядя на код, мы проверяем, привязан ли уже Timer id к затронутому объекту, а если нет, то запускаем новый таймер, который будет слегка изменять положение каждый раз при запуске. Мы запускаем таймер каждые несколько сотых секунды, чтобы переместить объект на небольшое расстояние, используя соотношение синуса и косинуса для описания круга, а также вращая его на градус, чтобы добиться в целом плавного внешнего вида анимации. Это работает нормально, но вы заметите, что он имеет тенденцию прыгать из своего начального положения вправо, прежде чем начать движение по кругу, поскольку вычисление синуса / косинуса резко переместит его в положение периметра. Кроме того, если вы коснетесь его еще раз, чтобы остановить и запустить анимацию, вы увидите, что она имеет тенденцию прыгать в позиции, поскольку мы не поддерживаем последнее значение градуса в состоянии. Давайте улучшим это, сохранив текущую переменную смещения walkDegree внутри объекта, инициализировав ее после ее создания в операторе then =>. Мы также будем использовать постоянную rotateDegree, чтобы поддерживать постоянную скорость вращения, чтобы она не ускорялась постоянно. Наконец, для первых нескольких сотен кадров анимации мы будем медленно увеличивать трансляции движения, чтобы избежать скачка от центра круговой траектории к периметру.

            arPlane.addSphere({
                radius: sphereRadius,
                position: {
                    x: position.x,
                    y: position.y + sphereRadius,
                    z: position.z - 0.5 //show it further away from camera
                },
                rotation: isIOS ? { x: -20, y: 0, z: 0 } : { x: 0, y: 100, z: -20 },
                materials: [{
                    diffuse: { contents: "SevenStar.fw.png", wrapMode: "Clamp" },
                }],
                onLongPress: interaction => {
                    console.log("Sphere was longpressed");
                    interaction.node.remove()
                },
                onTap: interaction => {
                    console.log("Sphere was tapped at coordinates " + interaction.touchPosition.x + " x " + interaction.touchPosition.y);
                    const rotateDegree = 1;
                    const radius = 1;
                    const fps = 120;
                    if (!interaction.node.animTimer) { //start the animation
                        interaction.node.animTimer = setInterval(() => {
                            interaction.node.walkDegrees += 1 / fps
                            if (interaction.node.walkDegrees <= 10) {
                                interaction.node.newX = interaction.node.position.x - Math.cos(interaction.node.walkDegrees) * radius * interaction.node.walkDegrees / 10
                                interaction.node.newY = interaction.node.position.y
                                interaction.node.newZ = interaction.node.position.z - Math.sin(interaction.node.walkDegrees) * radius * interaction.node.walkDegrees / 10
                            } else {
                                interaction.node.newX = interaction.node.position.x - Math.cos(interaction.node.walkDegrees) * radius
                                interaction.node.newY = interaction.node.position.y
                                interaction.node.newZ = interaction.node.position.z - Math.sin(interaction.node.walkDegrees) * radius
                            }
                            interaction.node.moveTo({ x: interaction.node.newX, y: interaction.node.newY, z: interaction.node.newZ });
                            interaction.node.rotateBy({
                                x: 0,
                                y: rotateDegree,
                                z: 0
                            });
                        }, 1000 / fps);
                    } else { //stop the animation
                        clearInterval(interaction.node.animTimer)
                        interaction.node.animTimer = null                        
                    }
                },
                draggingEnabled: false, //Android only
                rotatingEnabled: false, //Android only
                scalingEnabled: false, //Android only
                segmentCount: 100,
                mass: 0, //iOS only
            }).then(arNode => {
                console.log("Sphere successfully added");
                arNode.walkDegrees = 0;
            })

Добавление пользовательского интерфейса

Наше текущее представление AR отображается в полноэкранном режиме, но мы также можем наложить некоторые элементы пользовательского интерфейса поверх него, чтобы пользователь мог взаимодействовать со сценой другими способами или представлять информацию, не занимая место макета на устройстве. В этой публикации мы добавим текст вверху, чтобы пользователь знал, что он должен коснуться плоской поверхности, чтобы добавить сферу. Мы также добавим кнопку для удаления всех объектов со сцены, чтобы они могли начать заново с пустой сцены AR. Для этого нам нужно сначала упорядочить объявления GridLayout с тегом AR, а затем с любыми дополнительными тегами пользовательского интерфейса, чтобы NativeScript отображал их поверх (более высокий z-индекс), чем представление AR.

Добавьте следующее после тега AR в разделе XML файла:

            <GridLayout rows="auto" columns="*, auto" verticalAlignment="top">
                <Label row="0" col="0" text="Tap on a plane to start!" class="ar-text" textWrap="true" />
                <Label row="0" col="1" text="Clear" class="ar-button" @tap="clearModels()" />
            </GridLayout>

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

    data() {
        return {
            planeMaterial: new Color("white"),
            objects: [],
        }
    },

В разделе кода then после добавления новой сферы мы отправим ссылку на новый объект в этот массив, используя this.objects.push(arNode). Мы изменим обработчик длительного нажатия, чтобы удалить ссылку на объект из этого массива всякий раз, когда пользователь удаляет отдельную сферу из сцены, используя следующее перед удалением modelthis.objects.splice(this.objects.indexOf(interaction.node), 1). Наконец, мы добавим определение функции для clearModels, вызываемой кнопкой:

        clearModels() {
            this.objects.forEach(model => {
                model.remove()
            })
        },

Запустите приложение с этими изменениями на устройстве iOS, и вы увидите что-то вроде:

Сделанный!

Это все для этого поста. Если вы хотите скачать окончательные исходные файлы, вы можете найти их на Github.

Первоначально опубликовано на https://blog.angelengineering.com 15 мая 2020 г.

Примечание от Plain English

Вы знали, что мы запустили канал на YouTube? Каждое видео, которое мы снимаем, будет направлено на то, чтобы научить вас чему-то новому. Проверьте нас, нажав здесь, и обязательно подпишитесь на канал 😎