ARKit 2 в Xamarin.iOS
ARKit значительно зрел с момента его внедрения в прошлом году в iOS 11. В первую очередь, теперь можно обнаружить вертикальные, а также горизонтальные плоскости, что значительно повышает практическисть возможностей дополненной реальности в помещении. Кроме того, существуют новые возможности:
- Распознавание эталонных изображений и объектов в качестве соединения между реальным миром и цифровыми изображениями
- Новый режим освещения, имитирующий реальное освещение
- Возможность совместного использования и сохранения сред AR
- Новый формат файла, предпочтительный для хранения содержимого AR
Распознавание ссылочных объектов
Одна из демонстраций функций в ARKit 2 — это возможность распознавать эталонные изображения и объекты. Эталонные изображения можно загрузить из обычных файлов изображений (обсуждалось позже), но эталонные объекты должны быть проверены с помощью ориентированного ARObjectScanningConfiguration
на разработчика.
Пример приложения: сканирование и обнаружение трехмерных объектов
Пример — это порт проекта Apple, демонстрирующего следующее:
- Управление состоянием приложения с помощью
NSNotification
объектов - Настраиваемая визуализация
- Сложные жесты
- Сканирование объектов
- Хранение
ARReferenceObject
Сканирование эталонного объекта является батареей и процессором, интенсивным и старыми устройствами часто возникают проблемы с достижением стабильного отслеживания.
Управление состоянием с помощью объектов NSNotification
В этом приложении используется компьютер состояния, который переходит между следующими состояниями:
AppState.StartARSession
AppState.NotReady
AppState.Scanning
AppState.Testing
Кроме того, используется внедренный набор состояний и переходов при выполнении AppState.Scanning
:
Scan.ScanState.Ready
Scan.ScanState.DefineBoundingBox
Scan.ScanState.Scanning
Scan.ScanState.AdjustingOrigin
Приложение использует реактивную архитектуру, которая публикует уведомления NSNotificationCenter
о переходе состояния и подписывается на эти уведомления. Программа установки выглядит следующим образом ViewController.cs
:
// Configure notifications for application state changes
var notificationCenter = NSNotificationCenter.DefaultCenter;
notificationCenter.AddObserver(Scan.ScanningStateChangedNotificationName, State.ScanningStateChanged);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxCreatedNotificationName, State.GhostBoundingBoxWasCreated);
notificationCenter.AddObserver(ScannedObject.GhostBoundingBoxRemovedNotificationName, State.GhostBoundingBoxWasRemoved);
notificationCenter.AddObserver(ScannedObject.BoundingBoxCreatedNotificationName, State.BoundingBoxWasCreated);
notificationCenter.AddObserver(BoundingBox.ScanPercentageChangedNotificationName, ScanPercentageChanged);
notificationCenter.AddObserver(BoundingBox.ExtentChangedNotificationName, BoundingBoxExtentChanged);
notificationCenter.AddObserver(BoundingBox.PositionChangedNotificationName, BoundingBoxPositionChanged);
notificationCenter.AddObserver(ObjectOrigin.PositionChangedNotificationName, ObjectOriginPositionChanged);
notificationCenter.AddObserver(NSProcessInfo.PowerStateDidChangeNotification, DisplayWarningIfInLowPowerMode);
Типичный обработчик уведомлений обновит пользовательский интерфейс и, возможно, изменит состояние приложения, например этот обработчик, который обновляется по мере сканирования объекта:
private void ScanPercentageChanged(NSNotification notification)
{
var pctNum = TryGet<NSNumber>(notification.UserInfo, BoundingBox.ScanPercentageUserKey);
if (pctNum == null)
{
return;
}
double percentage = pctNum.DoubleValue;
// Switch to the next state if scan is complete
if (percentage >= 100.0)
{
State.SwitchToNextState();
}
else
{
DispatchQueue.MainQueue.DispatchAsync(() => navigationBarController.SetNavigationBarTitle($"Scan ({percentage})"));
}
}
Наконец, Enter{State}
методы изменяют модель и ПОЛЬЗОВАТЕЛЬСКИЙ интерфейс в соответствии с новым состоянием:
internal void EnterStateTesting()
{
navigationBarController.SetNavigationBarTitle("Testing");
navigationBarController.ShowBackButton(false);
loadModelButton.Hidden = true;
flashlightButton.Hidden = false;
nextButton.Enabled = true;
nextButton.SetTitle("Share", UIControlState.Normal);
testRun = new TestRun(sessionInfo, sceneView);
TestObjectDetection();
CancelMaxScanTimeTimer();
}
Настраиваемая визуализация
Приложение показывает низкоуровневое облако точки объекта, содержащегося в ограничивающем поле, проецируемого на обнаруженную горизонтальную плоскость.
Это облачная точка доступна разработчикам в свойстве ARFrame.RawFeaturePoints
. Визуализация облака точки эффективно может быть сложной проблемой. Перебор по точкам, а затем создание и размещение нового узла SceneKit для каждой точки убьет частоту кадров. Кроме того, если это делается асинхронно, будет задержка. Пример поддерживает производительность с помощью трех частей стратегии:
- Использование небезопасного кода для закрепления данных на месте и интерпретации данных как необработанного буфера байтов.
- Преобразование этого необработанного буфера в
SCNGeometrySource
объект и создание объекта "template".SCNGeometryElement
- Быстрое "стежка вместе" необработанных данных и шаблона с помощью
SCNGeometry.Create(SCNGeometrySource[], SCNGeometryElement[])
internal static SCNGeometry CreateVisualization(NVector3[] points, UIColor color, float size)
{
if (points.Length == 0)
{
return null;
}
unsafe
{
var stride = sizeof(float) * 3;
// Pin the data down so that it doesn't move
fixed (NVector3* pPoints = &points[0])
{
// Important: Don't unpin until after `SCNGeometry.Create`, because geometry creation is lazy
// Grab a pointer to the data and treat it as a byte buffer of the appropriate length
var intPtr = new IntPtr(pPoints);
var pointData = NSData.FromBytes(intPtr, (System.nuint) (stride * points.Length));
// Create a geometry source (factory) configured properly for the data (3 vertices)
var source = SCNGeometrySource.FromData(
pointData,
SCNGeometrySourceSemantics.Vertex,
points.Length,
true,
3,
sizeof(float),
0,
stride
);
// Create geometry element
// The null and bytesPerElement = 0 look odd, but this is just a template object
var template = SCNGeometryElement.FromData(null, SCNGeometryPrimitiveType.Point, points.Length, 0);
template.PointSize = 0.001F;
template.MinimumPointScreenSpaceRadius = size;
template.MaximumPointScreenSpaceRadius = size;
// Stitch the data (source) together with the template to create the new object
var pointsGeometry = SCNGeometry.Create(new[] { source }, new[] { template });
pointsGeometry.Materials = new[] { Utilities.Material(color) };
return pointsGeometry;
}
}
}
Результат выглядит следующим образом.
Сложные жесты
Пользователь может масштабировать, поворачивать и перетаскивать ограничивающий прямоугольник, который окружает целевой объект. В связанных распознавтелях жестов есть две интересные вещи.
Во-первых, все распознаватели жестов активируются только после прохождения порогового значения; Например, палец перетаскивал столько пикселей или поворот превышает какой-то угол. Метод заключается в том, чтобы накапливать перемещение до превышения порогового значения, а затем применить его постепенно:
// A custom rotation gesture recognizer that fires only when a threshold is passed
internal partial class ThresholdRotationGestureRecognizer : UIRotationGestureRecognizer
{
// The threshold after which this gesture is detected.
const double threshold = Math.PI / 15; // (12°)
// Indicates whether the currently active gesture has exceeded the threshold
private bool thresholdExceeded = false;
private double previousRotation = 0;
internal double RotationDelta { get; private set; }
internal ThresholdRotationGestureRecognizer(IntPtr handle) : base(handle)
{
}
// Observe when the gesture's state changes to reset the threshold
public override UIGestureRecognizerState State
{
get => base.State;
set
{
base.State = value;
switch(value)
{
case UIGestureRecognizerState.Began :
case UIGestureRecognizerState.Changed :
break;
default :
// Reset threshold check
thresholdExceeded = false;
previousRotation = 0;
RotationDelta = 0;
break;
}
}
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
if (thresholdExceeded)
{
RotationDelta = Rotation - previousRotation;
previousRotation = Rotation;
}
if (! thresholdExceeded && Math.Abs(Rotation) > threshold)
{
thresholdExceeded = true;
previousRotation = Rotation;
}
}
}
Вторая интересная вещь, выполняемая в связи с жестами, заключается в том, как ограничивающий прямоугольник перемещается в отношении обнаруженных реальных самолетов. Этот аспект рассматривается в этой записи блога Xamarin.
Другие новые возможности в ARKit 2
Дополнительные конфигурации отслеживания
Теперь вы можете использовать любой из следующих элементов в качестве основы для смешанной реальности:
- Только акселерометр устройства (
AROrientationTrackingConfiguration
iOS 11) - Лица (
ARFaceTrackingConfiguration
iOS 11) - Эталонные изображения (
ARImageTrackingConfiguration
iOS 12) - Сканирование трехмерных объектов (
ARObjectScanningConfiguration
iOS 12) - Визуальная инерционная odometry (
ARWorldTrackingConfiguration
улучшена в iOS 12)
AROrientationTrackingConfiguration
, рассмотренный в этой записи блога и примере F#, является самым ограниченным и обеспечивает плохой опыт смешанной реальности, так как он только помещает цифровые объекты в связи с движением устройства, не пытаясь связать устройство и экран в реальном мире.
Это ARImageTrackingConfiguration
позволяет распознавать реальные 2D-изображения (картины, логотипы и т. д.) и использовать их для привязки цифровых изображений:
var imagesAndWidths = new[] {
("cover1.jpg", 0.185F),
("cover2.jpg", 0.185F),
//...etc...
("cover100.jpg", 0.185F),
};
var referenceImages = new NSSet<ARReferenceImage>(
imagesAndWidths.Select( imageAndWidth =>
{
// Tuples cannot be destructured in lambda arguments
var (image, width) = imageAndWidth;
// Read the image
var img = UIImage.FromFile(image).CGImage;
return new ARReferenceImage(img, ImageIO.CGImagePropertyOrientation.Up, width);
}).ToArray());
configuration.TrackingImages = referenceImages;
Существует два интересных аспекта этой конфигурации:
- Это эффективно и может использоваться с потенциально большим количеством эталонных изображений
- Цифровое изображение привязано к изображению, даже если это изображение перемещается в реальном мире (например, если обложка книги распознается, она будет отслеживать книгу, как она вытащила полку, лежала и т. д.).
ARObjectScanningConfiguration
Ранее обсуждалась и является конфигурацией, ориентированной на разработчика, для сканирования трехмерных объектов. Он является высокопроцессорным и зарядным зарядом и не должен использоваться в приложениях конечных пользователей.
Окончательная конфигурация ARWorldTrackingConfiguration
отслеживания — это рабочая лошадка большинства возможностей смешанной реальности. Эта конфигурация использует "визуальную инерционную odometry" для связи реальных "точек признаков" с цифровыми изображениями. Цифровые геометрии или спрайты привязаны относительно реальных и вертикальных плоскостей или относительно обнаруженных ARReferenceObject
экземпляров. В этой конфигурации источник мира является исходной позицией камеры в пространстве с оси Z, выровненной по тяжести, и цифровые объекты "остаются на месте" относительно объектов в реальном мире.
Распознавание окружающей среды
ARKit 2 поддерживает "экологический текст", который использует захваченные изображения для оценки освещения и даже применения зрительных выделений к блестящим объектам. Карта экологических кубов создается динамически, и после того, как камера посмотрела во всех направлениях, может создать впечатляющий реалистичный опыт:
Чтобы использовать экологические тексты, выполните приведенные далее действия.
- Объекты
SCNMaterial
должны использоватьSCNLightingModel.PhysicallyBased
и назначать значение в диапазоне от 0 до 1 иMetalness.Contents
Roughness.Contents
- Конфигурация отслеживания должна задать:
EnvironmentTexturing
=AREnvironmentTexturing.Automatic
var sphere = SCNSphere.Create(0.33F);
sphere.FirstMaterial.LightingModelName = SCNLightingModel.PhysicallyBased;
// Shiny metallic sphere
sphere.FirstMaterial.Metalness.Contents = new NSNumber(1.0F);
sphere.FirstMaterial.Roughness.Contents = new NSNumber(0.0F);
// Session configuration:
var configuration = new ARWorldTrackingConfiguration
{
PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
LightEstimationEnabled = true,
EnvironmentTexturing = AREnvironmentTexturing.Automatic
};
Хотя идеально отраженная текстура, показанная в приведенном выше фрагменте кода, весело в примере, экологические текстуры, вероятно, лучше использовать с сдержанностью, чтобы она вызвала ответ "uncanny valley" (текстура является только оценкой на основе того, что камера записана).
Общие и постоянные интерфейсы AR
Еще одним основным дополнением ARWorldMap
к ARKit 2 является класс, который позволяет совместно использовать или хранить данные отслеживания мира. Вы получаете текущую карту мира с ARSession.GetCurrentWorldMapAsync
или GetCurrentWorldMap(Action<ARWorldMap,NSError>)
:
// Local storage
var PersistentWorldPath => Environment.GetFolderPath(Environment.SpecialFolder.Personal) + "/arworldmap";
// Later, after scanning the environment thoroughly...
var worldMap = await Session.GetCurrentWorldMapAsync();
if (worldMap != null)
{
var data = NSKeyedArchiver.ArchivedDataWithRootObject(worldMap, true, out var err);
if (err != null)
{
Console.WriteLine(err);
}
File.WriteAllBytes(PersistentWorldPath, data.ToArray());
}
Чтобы предоставить общий доступ к карте мира или восстановить ее, выполните следующие действия:
- Загрузка данных из файла,
- Разархивив его в
ARWorldMap
объект, - Используйте это в качестве значения для
ARWorldTrackingConfiguration.InitialWorldMap
свойства:
var data = NSData.FromArray(File.ReadAllBytes(PersistentWorldController.PersistenWorldPath));
var worldMap = (ARWorldMap)NSKeyedUnarchiver.GetUnarchivedObject(typeof(ARWorldMap), data, out var err);
var configuration = new ARWorldTrackingConfiguration
{
PlaneDetection = ARPlaneDetection.Horizontal | ARPlaneDetection.Vertical,
LightEstimationEnabled = true,
EnvironmentTexturing = AREnvironmentTexturing.Automatic,
InitialWorldMap = worldMap
};
Только ARWorldMap
содержит невидимые данные отслеживания мира и ARAnchor
объекты, они не содержат цифровые ресурсы. Чтобы предоставить общий доступ к геометрии или изображениям, необходимо разработать собственную стратегию, соответствующую вашему варианту использования (возможно, путем хранения и передачи только расположения и ориентации геометрии и применения его к статическим SCNGeometry
или, возможно, путем хранения и передачи сериализованных объектов). Преимущество ARWorldMap
заключается в том, что ресурсы после размещения относительно общего ARAnchor
доступа будут отображаться последовательно между устройствами или сеансами.
Формат файла описания универсальной сцены
Последней функцией заголовка ARKit 2 является принятие Apple универсального формата описания сцены Pixar. Этот формат заменяет формат DAE Collada в качестве предпочтительного формата для совместного использования и хранения ресурсов ARKit. Поддержка визуализации ресурсов встроена в iOS 12 и Mojave. Расширение ФАЙЛА USDZ — это несжатый и незашифрованный ZIP-архив, содержащий ФАЙЛЫ USD. Pixar предоставляет средства для работы с файлами USD, но пока нет много сторонней поддержки.
Советы по программированию ARKit
Управление ресурсами вручную
В ARKit важно вручную управлять ресурсами. Это не только позволяет высокой частоте кадров, на самом деле необходимо избежать запутанной "заморозки экрана". Платформа ARKit отложена о предоставлении нового кадра камеры (ARSession.CurrentFrame
. До тех пор, пока текущий ARFrame
Dispose()
вызов на него, ARKit не будет предоставлять новый кадр! Это приводит к тому, что видео будет "заморозить", несмотря на то, что остальная часть приложения реагирует. Решение — всегда обращаться ARSession.CurrentFrame
к блоку using
или вручную вызывать Dispose()
его.
Все объекты, производные от NSObject
них, реализуют IDisposable
NSObject
шаблон Dispose, поэтому обычно следует следовать этому шаблону для реализации Dispose
в производном классе.
Управление матрицами преобразования
В любом трехмерном приложении вы будете иметь дело с матрицами преобразования 4x4, которые компактно описывают перемещение, поворот и сдвиг объекта через трехмерное пространство. В SceneKit это SCNMatrix4
объекты.
Свойство SCNNode.Transform
возвращает матрицу преобразования для SCNNode
типа с поддержкой SCNMatrix4
основного simdfloat4x4
типа строки. Например, например:
var node = new SCNNode { Position = new SCNVector3(2, 3, 4) };
var xform = node.Transform;
Console.WriteLine(xform);
// Output is: "(1, 0, 0, 0)\n(0, 1, 0, 0)\n(0, 0, 1, 0)\n(2, 3, 4, 1)"
Как видно, позиция закодирована в первых трех элементах нижней строки.
В Xamarin общий тип для управления матрицами преобразования является NVector4
то, что по соглашению интерпретируется в основном столбце. То есть компонент перевода и позиции ожидается в M14, M24, M34, а не M41, M42, M43:
Согласование с выбором интерпретации матрицы жизненно важно для правильного поведения. Так как матрицы преобразования 3D имеют значение 4x4, ошибки согласованности не будут создавать никаких исключений во время компиляции или даже во время выполнения. Это просто то, что операции будут действовать неожиданно. Если объекты SceneKit / ARKit, кажется, застряли, улетают или дрожут, неправильное преобразование матрицы является хорошей возможностью. Решение просто NMatrix4.Transpose
: выполняется транспозиция элементов на месте.