Портал, манулы и мячи: опыт разработки для Apple Vision Pro. Часть 2

В первой части статьи я рассказал о двух мини-играх, которые мы с командой разработали для Apple Vision Pro для хакатона Ludum Dare, и особенностях работы с дополненной реальностью. В этой — поделюсь опытом индивидуальной разработки для гарнитуры, общими впечатлениями о работе с ней и несколькими идеями разработки под неё. 

Об авторе

Илья Проскуряков — iOS-разработчик в компании Effective, опыт работы 1,5 года. Участник конференций KODE Waves и DevFest, организатор и ментор Студенческой IT-лаборатории (г. Омск).

Мини-игра № 3 «Теннис»

Раз уж я добрался до Apple Vision Pro, мне хотелось поработать и с физикой, на которую на хакатоне не хватило времени. Я решил сделать мини-игру наподобие тенниса.

Первым делом мне нужно было понять, как у объектов работает физика. Для этого я подгрузил соответствующий ассет и добавил ему PhysicsMotionComponent, отвечающий за движения объектов. Ему я добавил коллизии и физическое тело, которое отвечает за центр массы, коэффициент упругости, коэффициент трения и прочие вещи. Это можно сделать как в Reality Composer Pro, так и в коде.

  func loadTennisBall() async {
        if let ball = try? await Entity(named: "TennisBall", in: realityKitContentBundle) {
            ball.scale = [0.7, 0.7, 0.7]
            ball.position.z = -1
            ball.position.y = 0.5
            ball.components.set(InputTargetComponent())
            ball.components[PhysicsMotionComponent.self] = .init()
            ball.name = "ball"
            rootEntity.addChild(ball)
        }
    }

Работа с физикой.

Работа с физикой.

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

Для этого я вернулся к ARKit и его сессии. В качестве дата-провайдера мне нужно было отдать ему SceneReconstructionProvider, который отвечает за отслеживание всех поверхностей вокруг.

  let session = ARKitSession()
  let sceneReconstruction = SceneReconstructionProvider()

Его также можно закинуть в метод Run, чтобы на апдейты sceneReconstruction пришли якоря стен, полов и прочих вещей, у которых были бы нужные параметры. Используя эти параметры, можно воссоздать форму якоря, например стены.

 .task {
            do {
                try await model.session.run([model.sceneReconstruction, model.handTracking])
            } catch {
                await dismissImmersiveSpace()
                openWindow(id: "error")
            }
        }

На основе пришедшей формы я создал сущность, в которую положил компонент CollisionComponent. Я задал ему физическое тело и расположение через расположение пришедшего якоря. 

    func processReconstructionUpdates() async {
        for await update in sceneReconstruction.anchorUpdates {
            let meshAnchor = update.anchor
            guard let shape = try? await ShapeResource.generateStaticMesh(from: meshAnchor) else { continue }
case .added:
                let entity = ModelEntity()
                entity.transform = Transform(matrix: meshAnchor.originFromAnchorTransform)
                entity.collision = CollisionComponent(shapes: [shape], isStatic: true)
                entity.components.set(InputTargetComponent())
                
                entity.physicsBody = PhysicsBodyComponent(mode: .static)

По сути, мы не можем напрямую взаимодействовать с реальными объектами, но можем отследить их форму, расположение и создать виртуальные копии, с которыми смогут взаимодействовать наши виртуальные объекты.

Кстати, чтобы уметь это отслеживать, нужно получить разрешение от пользователя. Для этого я создал два ключа ключа с описанием в info.plist (заявка на разрешение).

Получение разрешения от пользователя.

Получение разрешения от пользователя.

Мяч, не ведающий преград VS мяч-мыло

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

Исследуя возможности гарнитуры, я выяснил, что могу отслеживать не только руку, но и каждый её сустав. Как это сделать?  

Первым делом создаём словарь, ключом которого будет сустав. Значение по ключу — 3D-сущность, у которой будет физический компонент и компонент CollisionShape. Так 3D-сущность прикрепляется к реальному пальцу и помогает ему взаимодействовать с игровым пространством.

enum HandEntity: Hashable {
    case left(HandSkeleton.JointName)
    case right(HandSkeleton.JointName)
}
    private let fingerEntities: [HandEntity: ModelEntity] = [
        .left(.indexFingerTip): createFingertip(name: "hand"),
        .left(.indexFingerKnuckle): createFingertip(),
        .left(.indexFingerMetacarpal): createFingertip(),

Функция генерации объекта, прикрепляемого к суставу, выглядит так:

func createFingertip(name: String? = nil) -> ModelEntity {
    let entity = ModelEntity(
        mesh: .generateSphere(radius: 0.005),
        materials: [UnlitMaterial(color: .red)],
        collisionShape: .generateSphere(radius: 0.005),
        mass: 0.0)
    entity.components.set(PhysicsBodyComponent(mode: .kinematic))
//    entity.components.set(OpacityComponent(opacity: 1.0))
    if let name = name {
        entity.name = name
    }
    return entity
}

В том же методе, где я отслеживал руки, можно следить и за пальцами.

Так я получил две координаты: положение руки относительно мира и положение суставов относительно руки. Перемножив эти матрицы, я получил координату сустава относительно мира.

 let originFromJoint = handAnchor.originFromAnchorTransform * joint.anchorFromJointTransform

Получившееся изменение внёс в словарь. Я думал, что этого будет достаточно, чтобы реалистично ухватить мяч. 

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

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

За образец я взял игру Happy Beam от Apple. В ней на пользователя летят грустные тучки, а он жестами в форме сердечек отправляет им лучи добра, после чего они превращаются в счастливые облачка.

Я посмотрел, как они отслеживают жест, и сделал собственный метод. Сохранив отслеживание суставов рук и их локации, я выбрал большой, указательный и безымянный пальцы и задал максимальное расстояние между ними в 6 см (подбирал на глаз). Для определения жеста этого достаточно. 


        let indexAndRingFingersDistance = distance(originFromLeftHandIndexTipTransform, originFromLeftHandRingFingerTipTransform)
        let thumbAndIndexFingersDistance = distance(originFromLeftHandIndexTipTransform, originFromLeftHandThumbTipTransform)
        let isGrabGesture = indexAndRingFingersDistance < 0.06 && thumbAndIndexFingersDistance < 0.06
        return isGrabGesture

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

Для этого у content, который передаётся в closure RealityView, я вызвал метод subscribe () и подписался на все коллизии, происходящие в приложении. 

         collisionSubscription = content.subscribe(to: CollisionEvents.Began.self, on: nil, componentType: nil) { event in
                Task {
                    try await handleHandCollisionStart(for: event, isGrabed: model.computeTransformOfUserPerformedGrabGesture())
                }
            }

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

    if event.entityA.name == "hand" && event.entityB.children.first?.name == "Tennis_Ball" {
        handEntity = event.entityA
        otherEntity = event.entityB.children.first ?? Entity()

Выглядит это так: происходит какая-то коллизия, и начинается проверка, был ли в этот момент жест и правильные ли объекты — рука и мяч — провзаимодействовали. Если всё совпадает, мяч прикрепляется к центру ладони.

    otherEntity.components[AnchoringComponent.self] = .init(AnchoringComponent.Target.hand(.left, location: .palm), trackingMode: .continuous)

Проверка показала, что мяч перестал скользить –, но перестал и отделяться от ладони в принципе.

Я успел превратить мяч-мыло в мяч-слайм, который не отлеплялся от руки, — и время, отведённое мне на эксперименты, вышло.

Впечатления от работы с visionOS

Начну, как полагается, с плюсов

  1. Это интересно, потому что разработка под AR — это кайф. 

  2. Декларативный подход: многие функции, сложные с инженерной точки зрения (например, загрузка 3D-объекта и его размещение в AR), можно выполнить несколькими строками кода.

  3. Я получил практический опыт 3D-разработки. Это было непросто, но я начал разбираться, как работать с физикой объекта, как располагать его в 3D-пространстве и прочем.

Недостатков чуть больше

  1. Фреймворки ARKit и RealityKit находятся в бете, а сама технология пока что малопопулярна из-за высокой стоимости гарнитуры.
    Как следствие, у разработчика есть небольшой объём информации. Примеров тоже немного, поэтому часто приходится искать решение ручным перебором вариантов. Работа с документацией также затруднена, но в основном это связано со спецификой работы с 3D (поскольку это не моя сфера).
    Масла в огонь подливают и подобные случаи: я взял код с wwdc2023, добавил к себе — и он не сработал. Выяснилось, что Apple изменили работу функции, а отразить это в документации не успели.
    Как следствие, даже небольшой функционал приходится писать долго. С другой стороны, разобравшись с этим самостоятельно, можно стать одним из первых экспертов в этой теме.

  2. Необходимость работы с реальной гарнитурой, потому что симулятор сильно урезан в возможностях: в нём невозможно отслеживать реальный мир. Попытки запустить в симуляторе приложение с таким отслеживанием привели к крашу приложения.
    Однако если приложить дополнительные усилия и написать логику симуляции реального мира (замокать упомянутые ранее провайдеры), работа с симулятором станет проще.

  3. Проблема с 3D-моделями для AR: как с поиском качественных вариантов, так и с созданием собственных. 

Я пишу только о тех плюсах и минусах, которые увидел во время работы с гарнитурой. Полный инструментарий разработки для неё я физически не смог бы освоить за отведённое время, и за бортом остались многие интересные моменты. В том числе:
— разработка под Apple Vision Pro на Unity;
— Shader Graph в Reality Composer Pro — инструмент для создания сложных объектов и эффектов с помощью узлов, соединённых в граф;
— рендер контента с полным погружением (виртуальная реальность) с помощью фреймворка Metal. Он предоставляет приложению прямой доступ к графическому процессору устройства, который приложения могут использовать для быстрой визуализации сложных сцен. 

Так выглядит темплейт приложения Metal.

Так выглядит темплейт приложения Metal.

Области применения технологии

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

Перечислю несколько направлений, в которых приложения с AR могут, на мой взгляд, в корне изменить положение. 

  1. Медицина. Первое, что мне пришло в голову, — это, конечно же, 3D-модель человека, со всеми его органами и системами, которые можно многократно увеличивать. Причём можно сделать модели и для конкретных заболеваний, чтобы студенты, врачи и пациенты видели, как в реальности выглядят поражённые ткани и как на них влияет болезнь.

  2. Реальный кейс, предложенный мне после первых экспериментов с гарнитурой. К нам пришёл заказчик с проектом визуализации жилого комплекса, где в AR можно было увидеть расположение подземных парковок, лифтов и другой инфраструктуры.

  3. Гарнитура позволяет создавать виртуальные копии коммуникаций. Например,
    можно перенести в AR схему электропроводки помещения, в котором предстоит работать. Больше никаких наугад пробитых стен и раскопанных дорог — только точечная работа!
    Предполагаю, что это возможно, поскольку, по моим наблюдениям, Apple Vision Pro запоминают пространства, в которых работали. Я увидел, что очки видят не только стены комнаты и то, что в ней, но и остальные стены, которые человеку физически не видны.

  4. На Student Labs я был ментором команды, которая при помощи гарнитуры разработала приложение для брендинга продукции в смешанной реальности. Оно позволяет в реальном времени увидеть, как будет выглядеть дизайн, позволяя пользователю ускорить процесс брендирования.

Если у вас есть опыт подобной разработки, поделитесь им в комментариях. Я планирую и дальше работать с VisionOS, и о процессе буду рассказывать в телеграм-канале @effectiveband.

© Habrahabr.ru