Портал, манулы и мячи: опыт разработки для 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
Начну, как полагается, с плюсов
Это интересно, потому что разработка под AR — это кайф.
Декларативный подход: многие функции, сложные с инженерной точки зрения (например, загрузка 3D-объекта и его размещение в AR), можно выполнить несколькими строками кода.
Я получил практический опыт 3D-разработки. Это было непросто, но я начал разбираться, как работать с физикой объекта, как располагать его в 3D-пространстве и прочем.
Недостатков чуть больше
Фреймворки ARKit и RealityKit находятся в бете, а сама технология пока что малопопулярна из-за высокой стоимости гарнитуры.
Как следствие, у разработчика есть небольшой объём информации. Примеров тоже немного, поэтому часто приходится искать решение ручным перебором вариантов. Работа с документацией также затруднена, но в основном это связано со спецификой работы с 3D (поскольку это не моя сфера).
Масла в огонь подливают и подобные случаи: я взял код с wwdc2023, добавил к себе — и он не сработал. Выяснилось, что Apple изменили работу функции, а отразить это в документации не успели.
Как следствие, даже небольшой функционал приходится писать долго. С другой стороны, разобравшись с этим самостоятельно, можно стать одним из первых экспертов в этой теме.Необходимость работы с реальной гарнитурой, потому что симулятор сильно урезан в возможностях: в нём невозможно отслеживать реальный мир. Попытки запустить в симуляторе приложение с таким отслеживанием привели к крашу приложения.
Однако если приложить дополнительные усилия и написать логику симуляции реального мира (замокать упомянутые ранее провайдеры), работа с симулятором станет проще.Проблема с 3D-моделями для AR: как с поиском качественных вариантов, так и с созданием собственных.
Я пишу только о тех плюсах и минусах, которые увидел во время работы с гарнитурой. Полный инструментарий разработки для неё я физически не смог бы освоить за отведённое время, и за бортом остались многие интересные моменты. В том числе:
— разработка под Apple Vision Pro на Unity;
— Shader Graph в Reality Composer Pro — инструмент для создания сложных объектов и эффектов с помощью узлов, соединённых в граф;
— рендер контента с полным погружением (виртуальная реальность) с помощью фреймворка Metal. Он предоставляет приложению прямой доступ к графическому процессору устройства, который приложения могут использовать для быстрой визуализации сложных сцен.
Так выглядит темплейт приложения Metal.
Области применения технологии
Игровая индустрия хоть и привлекательна экономически, всё же не является единственным направлением, в котором развивается дополненная реальность. Технология стремительно осваивает прикладные сферы — от строительной до медицинской.
Перечислю несколько направлений, в которых приложения с AR могут, на мой взгляд, в корне изменить положение.
Медицина. Первое, что мне пришло в голову, — это, конечно же, 3D-модель человека, со всеми его органами и системами, которые можно многократно увеличивать. Причём можно сделать модели и для конкретных заболеваний, чтобы студенты, врачи и пациенты видели, как в реальности выглядят поражённые ткани и как на них влияет болезнь.
Реальный кейс, предложенный мне после первых экспериментов с гарнитурой. К нам пришёл заказчик с проектом визуализации жилого комплекса, где в AR можно было увидеть расположение подземных парковок, лифтов и другой инфраструктуры.
Гарнитура позволяет создавать виртуальные копии коммуникаций. Например,
можно перенести в AR схему электропроводки помещения, в котором предстоит работать. Больше никаких наугад пробитых стен и раскопанных дорог — только точечная работа!
Предполагаю, что это возможно, поскольку, по моим наблюдениям, Apple Vision Pro запоминают пространства, в которых работали. Я увидел, что очки видят не только стены комнаты и то, что в ней, но и остальные стены, которые человеку физически не видны.На Student Labs я был ментором команды, которая при помощи гарнитуры разработала приложение для брендинга продукции в смешанной реальности. Оно позволяет в реальном времени увидеть, как будет выглядеть дизайн, позволяя пользователю ускорить процесс брендирования.
Если у вас есть опыт подобной разработки, поделитесь им в комментариях. Я планирую и дальше работать с VisionOS, и о процессе буду рассказывать в телеграм-канале @effectiveband.